├── .DS_Store ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarn └── install-state.gz ├── .yarnrc ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RGSS3 ├── .gitignore ├── Gemfile ├── README.md ├── index.rb ├── modules │ ├── Audio.rb │ ├── RPG.rb │ └── Table.rb ├── plugins │ └── rxscript.rb └── test │ └── hangul.test.rb ├── images ├── dep.svg ├── icon-simple256x256.png ├── icon128x128.png └── icon256x256.png ├── jest.config.js ├── package.json ├── src ├── Helper.ts ├── JSerializeObject.ts ├── Mutex.ts ├── Packer.ts ├── RGSS.ts ├── Unpacker.ts ├── commands │ ├── CheckMigrationNeeded.ts │ ├── CheckRuby.ts │ ├── CheckWine.ts │ ├── DeleteCommand.ts │ ├── ExtractScriptFiles.ts │ ├── MenuCommand.ts │ ├── OpenGameFolder.ts │ ├── SetGamePath.ts │ └── TestGamePlay.ts ├── common │ ├── Buttons.ts │ ├── FileIndexTransformer.ts │ ├── Marshal.ts │ ├── MessageHelper.ts │ ├── ScriptListFile.ts │ ├── WorkspaceValue.ts │ └── encryption │ │ ├── DataManager.spec.ts │ │ ├── EncryptionManager.ts │ │ ├── ScriptStore.ts │ │ ├── Scripts.rvdata2 │ │ └── index.ts ├── events │ └── EventHandler.ts ├── extension.ts ├── providers │ ├── DependencyProvider.ts │ ├── RGSSScriptSection.ts │ ├── ScriptTree.ts │ ├── ScriptViewer.ts │ ├── StatusbarProvider.ts │ └── TreeFileWatcher.ts ├── services │ ├── ConfigService.ts │ └── LoggingService.ts ├── store │ └── GlobalStore.ts └── utils │ ├── Path.ts │ ├── Validator.ts │ └── uuid.ts ├── tests ├── .gitignore └── example │ ├── Scripts.rvdata2 │ ├── Test.rb │ └── test.dump ├── tsconfig.json ├── types ├── buttons.enum.js ├── buttons.enum.js.map └── global.d.ts ├── yarn-error.log └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .token 7 | **/.DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "[ruby]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "editor.formatOnSave": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version Log 2 | 3 | ## 1.0.0 - 10 Mar 2024 4 | 5 | - Added a new feature that can drag and drop 6 | 7 | ## 0.9.3 - 09 Dec 2023 8 | 9 | - Fixed an issue where filenames were always set to ASCII-8BIT [(#23)](https://github.com/biud436/vscode-rgss-script-compiler/issues/23) 10 | - Added stack trace code to the Ruby code. 11 | 12 | ## 0.9.2 - 01 Sep 2023 13 | 14 | - Added a new feature that can change the name of the script file in TreeViewer. 15 | - Fixed issue that can't open the TreeViewer when containing the dot more than one string inside script name. 16 | 17 | ## 0.9.0 - 30 Aug 2023 18 | 19 | - Fixed an issue that prevented extensions from being activated because of the database. 20 | 21 | ## 0.8.1 - 07 Jul 2023 22 | 23 | - Added a Script Explorer that allows users to add, remove, and refresh script files ([#14](https://github.com/biud436/vscode-rgss-script-compiler/issues/14)) 24 | - Added a feature to hide the status bar. 25 | - Added MKXP-Z support on Mac. 26 | - Debug mode support ([#16](https://github.com/biud436/vscode-rgss-script-compiler/issues/16)) 27 | - Added Linux support (PR [#21](https://github.com/biud436/vscode-rgss-script-compiler/pull/21)) 28 | 29 | ## 0.0.12 - 22 Mar 2022 30 | 31 | - Added a feature that can execute game when pressing the key called `F5` (#3) 32 | - Added a feature that can compile ruby files automatically when pressing the key called `ctrl + s` (#2) 33 | 34 | ## 0.0.10 - 09 Mar 2022 35 | 36 | - Added a new feature that can open the game folder in the visual studio code's workspace. 37 | - Allow the user to import scripts for RPG Maker XP on Windows or MacOS. 38 | - `README.md` was supplemented by using a translator. 39 | 40 | ## 0.0.5 - 08 Mar 2022 41 | 42 | - Removed `*` event that means to start the extension unconditionally when starting up the `vscode` 43 | 44 | ## 0.0.4 - 07 Mar 2022 45 | 46 | - Fixed the language of the hard coded logger message as in English. 47 | - Fixed the issue that line break character is changed empty string in `plugins/rxscript.tb` 48 | - Changed export folder name as `Scripts` 49 | 50 | ## 0.0.3 - 07 Mar 2022 51 | 52 | - Added a new event that can detect a file named `Game.ini` in the workspace folder. 53 | - Fixed the language of the hard coded logger message as in English. 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 JinSeok Eo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This extension allows you to edit scripts directly in Visual Studio Code without using the script editor of `RPG Maker VX Ace` or `RPG Maker XP`. 4 | 5 | ## Features 6 | 7 | - **Automatic Saving and Compilation**: Pressing `CTRL + S` saves your files and compiles your code automatically, so you don't have to worry about losing your progress. 8 | 9 | - **Test Play**: Pressing `F5` allows you to quickly test your code without having to run any additional commands. 10 | 11 | ## Screenshots 12 | 13 |

14 | 15 | ![scr2](https://github.com/biud436/vscode-rgss-script-compiler/assets/13586185/dee8d4c2-fc9a-467a-bec8-765b91453973) 16 | ![scr3](https://github.com/biud436/vscode-rgss-script-compiler/assets/13586185/64ab60a3-b55f-4b15-86b3-57318bc41cef) 17 | ![scr1](https://github.com/biud436/vscode-rgss-script-compiler/assets/13586185/44b1371c-2ddd-4acb-b608-030e1c504c49) 18 | 19 |

20 | 21 | # System Requirements 22 | 23 | ## Windows 24 | 25 | - Ruby version 2.6.8 or higher must be installed on your system. 26 | 27 | ## Mac 28 | 29 | - Ruby 2.6.10 or higher must be installed on your system. 30 | - [MKXP-Z](https://github.com/mkxp-z/mkxp-z) 31 | 32 | ## Linux 33 | 34 | - Ruby version 2.6.8 or higher must be installed on your system. 35 | - Wine (preferably the latest version) 36 | 37 | # Marketplace Link 38 | 39 | - [https://marketplace.visualstudio.com/items?itemName=biud436.rgss-script-compiler](https://marketplace.visualstudio.com/items?itemName=biud436.rgss-script-compiler) 40 | 41 | # Caution 42 | 43 | - Only one parent folder is allowed in the workspace. If there are multiple folders, only the top-level folder will be recognized. 44 | - The workspace is not automatically set to the initial game folder. This is because the workspace and game folders may be different. 45 | - This extension is not always active. The Game.ini file must be located in the root game folder to activate it. 46 | - The extension will activate the remaining buttons only when a game folder is selected. However, if the rgss-compiler.json file exists, it will be automatically activated. When VS Code starts up, the Import or Compile button will be automatically activated if this file exists. 47 | - Do not compile scripts while RPG Maker XP or RPG Maker VX Ace is running. 48 | 49 | # Supported tools 50 | 51 | - RPG Maker XP 52 | - RPG Maker VX Ace 53 | 54 | # Usage 55 | 56 | This extension is designed for use on macOS, Windows 11 and Linux. Before using this extension, you must first install `ruby 2.6.8` or higher on your local machine. 57 | 58 | To check if Ruby is installed on your computer, run this command in your terminal or command prompt: 59 | 60 | ```bash 61 | ruby -v 62 | ``` 63 | 64 | Ruby comes pre-installed on Mac, so you can ignore this step if you're on a Mac. I tried using a Node module like `Marshal` or a WASM-based Ruby because I didn't want to require a Ruby installation, but they were not stable. 65 | 66 | If Ruby is installed properly, you should see the version number displayed (e.g., `ruby 3.2.1`). 67 | 68 | If running this extension on Linux you will also need to install `Wine` on your system to support testing the game. 69 | 70 | To check if Wine is installed in your system you can run this command: 71 | 72 | ```bash 73 | wine --version 74 | ``` 75 | 76 | # Maintainer and Contributors 77 | 78 | - Extension Maintainer 79 | 80 | - Biud436 (https://github.com/biud436) 81 | 82 | - Contributors 83 | 84 | - SnowSzn (PR [#21](https://github.com/biud436/vscode-rgss-script-compiler/pull/21)) 85 | 86 | - `RGSS3/plugins/rxscript.rb` 87 | 88 | - Korokke (gksdntjr714@naver.com) 89 | 90 | - `RGSS3/modules/Table.rb` 91 | 92 | - CaptainJet (https://github.com/CaptainJet/RM-Gosu) 93 | 94 | - `RGSS3/RPG.rb` 95 | - Yoji Ojima (Gotcha Gotcha Games, KADOKAWA) 96 | -------------------------------------------------------------------------------- /RGSS3/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bundle/ 3 | vendor/ 4 | binstubs/ 5 | doc/ -------------------------------------------------------------------------------- /RGSS3/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rdoc' 4 | -------------------------------------------------------------------------------- /RGSS3/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Usage 4 | 5 | ```bash 6 | gem install bundler 7 | bundle install -j 4 8 | ``` 9 | 10 | if you wish to create documentation files, try to do as follows: 11 | 12 | ```bash 13 | rdoc 14 | ``` 15 | -------------------------------------------------------------------------------- /RGSS3/index.rb: -------------------------------------------------------------------------------- 1 | #!/bin/ruby 2 | require_relative './modules/Audio.rb' 3 | require_relative './modules/Table.rb' 4 | require_relative './modules/RPG.rb' 5 | require_relative './plugins/rxscript.rb' 6 | require 'optparse' 7 | require 'zlib' 8 | 9 | raise 'Ruby 1.9.2 or later is required.' if RUBY_VERSION <= '1.9.1' 10 | 11 | ## 12 | # +Entrypoint+ 13 | # Entry point for the application. 14 | # 15 | module EntryPoint 16 | ## 17 | # +App+ 18 | # This class allows you to handle by passing the arguments to the application. 19 | class App 20 | ## 21 | # Initialize the application. 22 | def initialize 23 | options = { compress: false } 24 | OptionParser 25 | .new do |opts| 26 | opts.banner = 'Usage: rvdata2 [options]' 27 | opts.on( 28 | '-o', 29 | '--output OUTPUT', 30 | 'Sets the VSCode Workspace Directory', 31 | ) { |v| options[:output] = v } 32 | opts.on( 33 | '-i', 34 | '--input INPUT', 35 | 'Sets the script file ends with named rvdata2', 36 | ) { |v| options[:input] = v } 37 | opts.on( 38 | '-c', 39 | '--compress', 40 | 'Compress script files to Scripts.rvdata2', 41 | ) { |v| options[:compress] = v } 42 | opts.on('-h', '--help', 'Prints the help documentaion') do 43 | puts opts 44 | exit 45 | end 46 | end 47 | .parse!(ARGV) 48 | 49 | if !options[:output] || !options[:input] 50 | raise 'Please specify the VSCode Workspace Directory and the script file ends with named rvdata2.' 51 | end 52 | 53 | @vscode_workspace = options[:output] 54 | @scripts_file = options[:input] 55 | @compress = options[:compress] 56 | end 57 | 58 | ## 59 | # 스크립트 파일을 내보냅니다. 60 | # 61 | def start 62 | if @compress 63 | compress_script(@vscode_workspace, @scripts_file) 64 | else 65 | extract_script(@vscode_workspace, @scripts_file) 66 | end 67 | end 68 | 69 | def is_windows? 70 | return RUBY_PLATFORM =~ /mswin(?!ce)|mingw|cygwin|bccwin/ 71 | end 72 | 73 | def is_mac? 74 | return RUBY_PLATFORM =~ /darwin/ 75 | end 76 | 77 | def is_hangul?(filename) 78 | return filename =~ /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/ 79 | end 80 | 81 | private 82 | 83 | ## 84 | # Extracts Script file. 85 | # 86 | # @param [String] vscode_workspace 87 | # @param [String] scripts_file 88 | # @return [void] 89 | def extract_script(vscode_workspace, scripts_file) 90 | begin 91 | root_folder = @vscode_workspace 92 | extract_folder = File.join(root_folder, 'Scripts').gsub('\\', '/') 93 | 94 | Dir.mkdir(extract_folder) if !File.exist?(extract_folder) 95 | 96 | RXDATA.ExtractScript(extract_folder, @scripts_file) 97 | rescue => e 98 | puts e 99 | end 100 | end 101 | 102 | ## 103 | # Compress Script file. 104 | # 105 | # @param [String] vscode_workspace 106 | # @param [String] scripts_file 107 | # @return [void] 108 | def compress_script(vscode_workspace, scripts_file) 109 | begin 110 | 111 | root_folder = @vscode_workspace 112 | extract_folder = File.join(root_folder, 'Scripts').gsub('\\', '/') 113 | 114 | Dir.mkdir(extract_folder) if !File.exist?(extract_folder) 115 | 116 | RXDATA.CompressScript(extract_folder, @scripts_file) 117 | rescue => e 118 | puts e 119 | puts e.backtrace 120 | end 121 | end 122 | end 123 | end 124 | 125 | $main = EntryPoint::App.new 126 | $main.start 127 | -------------------------------------------------------------------------------- /RGSS3/modules/Audio.rb: -------------------------------------------------------------------------------- 1 | module Audio 2 | module_function 3 | 4 | def setup_midi; end 5 | 6 | def bgm_play(filename, volume = 100, pitch = 100, pos = 0) 7 | 1 8 | end 9 | 10 | def bgm_stop; end 11 | 12 | def bgm_fade(time); end 13 | 14 | def bgm_pos 15 | 0 # Incapable of integration at the time 16 | end 17 | 18 | def bgs_play(filename, volume = 100, pitch = 100, pos = 0) 19 | 1 20 | end 21 | 22 | def bgs_stop 23 | @bgs.stop if @bgs 24 | end 25 | 26 | def bgs_fade(time); end 27 | 28 | def bgs_pos 29 | 0 30 | end 31 | 32 | def me_play(filename, volume = 100, pitch = 100) 33 | 1 34 | end 35 | 36 | def me_stop; end 37 | 38 | def me_fade(time); end 39 | 40 | def se_play(filename, volume = 100, pitch = 100) 41 | 1 42 | end 43 | 44 | def se_stop; end 45 | end 46 | -------------------------------------------------------------------------------- /RGSS3/modules/RPG.rb: -------------------------------------------------------------------------------- 1 | module RPG 2 | end 3 | 4 | class RPG::AudioFile 5 | def initialize(name = '', volume = 100, pitch = 100) 6 | @name = name 7 | @volume = volume 8 | @pitch = pitch 9 | end 10 | attr_accessor :name 11 | attr_accessor :volume 12 | attr_accessor :pitch 13 | end 14 | 15 | class RPG::BGM < RPG::AudioFile 16 | @@last = RPG::BGM.new 17 | def play(pos = 0) 18 | if @name.empty? 19 | Audio.bgm_stop 20 | @@last = RPG::BGM.new 21 | else 22 | Audio.bgm_play('Audio/BGM/' + @name, @volume, @pitch, pos) 23 | @@last = self.clone 24 | end 25 | end 26 | def replay 27 | play(@pos) 28 | end 29 | def self.stop 30 | Audio.bgm_stop 31 | @@last = RPG::BGM.new 32 | end 33 | def self.fade(time) 34 | Audio.bgm_fade(time) 35 | @@last = RPG::BGM.new 36 | end 37 | def self.last 38 | @@last.pos = Audio.bgm_pos 39 | @@last 40 | end 41 | attr_accessor :pos 42 | end 43 | 44 | class RPG::BGS < RPG::AudioFile 45 | @@last = RPG::BGS.new 46 | def play(pos = 0) 47 | if @name.empty? 48 | Audio.bgs_stop 49 | @@last = RPG::BGS.new 50 | else 51 | Audio.bgs_play('Audio/BGS/' + @name, @volume, @pitch, pos) 52 | @@last = self.clone 53 | end 54 | end 55 | def replay 56 | play(@pos) 57 | end 58 | def self.stop 59 | Audio.bgs_stop 60 | @@last = RPG::BGS.new 61 | end 62 | def self.fade(time) 63 | Audio.bgs_fade(time) 64 | @@last = RPG::BGS.new 65 | end 66 | def self.last 67 | @@last.pos = Audio.bgs_pos 68 | @@last 69 | end 70 | attr_accessor :pos 71 | end 72 | 73 | class RPG::ME < RPG::AudioFile 74 | def play 75 | if @name.empty? 76 | Audio.me_stop 77 | else 78 | Audio.me_play('Audio/ME/' + @name, @volume, @pitch) 79 | end 80 | end 81 | def self.stop 82 | Audio.me_stop 83 | end 84 | def self.fade(time) 85 | Audio.me_fade(time) 86 | end 87 | end 88 | 89 | class RPG::SE < RPG::AudioFile 90 | def play 91 | Audio.se_play('Audio/SE/' + @name, @volume, @pitch) unless @name.empty? 92 | end 93 | def self.stop 94 | Audio.se_stop 95 | end 96 | end 97 | 98 | class RPG::Tileset 99 | def initialize 100 | @id = 0 101 | @mode = 1 102 | @name = '' 103 | @tileset_names = Array.new(9).collect { '' } 104 | @flags = Table.new(8192) 105 | @flags[0] = 0x0010 106 | (2048..2815).each { |i| @flags[i] = 0x000F } 107 | (4352..8191).each { |i| @flags[i] = 0x000F } 108 | @note = '' 109 | end 110 | attr_accessor :id 111 | attr_accessor :mode 112 | attr_accessor :name 113 | attr_accessor :tileset_names 114 | attr_accessor :flags 115 | attr_accessor :note 116 | end 117 | class RPG::Map 118 | def initialize(width, height) 119 | @display_name = '' 120 | @tileset_id = 1 121 | @width = width 122 | @height = height 123 | @scroll_type = 0 124 | @specify_battleback = false 125 | @battleback_floor_name = '' 126 | @battleback_wall_name = '' 127 | @autoplay_bgm = false 128 | @bgm = RPG::BGM.new 129 | @autoplay_bgs = false 130 | @bgs = RPG::BGS.new('', 80) 131 | @disable_dashing = false 132 | @encounter_list = [] 133 | @encounter_step = 30 134 | @parallax_name = '' 135 | @parallax_loop_x = false 136 | @parallax_loop_y = false 137 | @parallax_sx = 0 138 | @parallax_sy = 0 139 | @parallax_show = false 140 | @note = '' 141 | @data = Table.new(width, height, 4) 142 | @events = {} 143 | end 144 | attr_accessor :display_name 145 | attr_accessor :tileset_id 146 | attr_accessor :width 147 | attr_accessor :height 148 | attr_accessor :scroll_type 149 | attr_accessor :specify_battleback 150 | attr_accessor :battleback1_name 151 | attr_accessor :battleback2_name 152 | attr_accessor :autoplay_bgm 153 | attr_accessor :bgm 154 | attr_accessor :autoplay_bgs 155 | attr_accessor :bgs 156 | attr_accessor :disable_dashing 157 | attr_accessor :encounter_list 158 | attr_accessor :encounter_step 159 | attr_accessor :parallax_name 160 | attr_accessor :parallax_loop_x 161 | attr_accessor :parallax_loop_y 162 | attr_accessor :parallax_sx 163 | attr_accessor :parallax_sy 164 | attr_accessor :parallax_show 165 | attr_accessor :note 166 | attr_accessor :data 167 | attr_accessor :events 168 | end 169 | 170 | class RPG::Map::Encounter 171 | def initialize 172 | @troop_id = 1 173 | @weight = 10 174 | @region_set = [] 175 | end 176 | attr_accessor :troop_id 177 | attr_accessor :weight 178 | attr_accessor :region_set 179 | end 180 | 181 | class RPG::MapInfo 182 | def initialize 183 | @name = '' 184 | @parent_id = 0 185 | @order = 0 186 | @expanded = false 187 | @scroll_x = 0 188 | @scroll_y = 0 189 | end 190 | attr_accessor :name 191 | attr_accessor :parent_id 192 | attr_accessor :order 193 | attr_accessor :expanded 194 | attr_accessor :scroll_x 195 | attr_accessor :scroll_y 196 | end 197 | 198 | class RPG::Event 199 | def initialize(x, y) 200 | @id = 0 201 | @name = '' 202 | @x = x 203 | @y = y 204 | @pages = [RPG::Event::Page.new] 205 | end 206 | attr_accessor :id 207 | attr_accessor :name 208 | attr_accessor :x 209 | attr_accessor :y 210 | attr_accessor :pages 211 | end 212 | 213 | class RPG::Event::Page 214 | def initialize 215 | @condition = RPG::Event::Page::Condition.new 216 | @graphic = RPG::Event::Page::Graphic.new 217 | @move_type = 0 218 | @move_speed = 3 219 | @move_frequency = 3 220 | @move_route = RPG::MoveRoute.new 221 | @walk_anime = true 222 | @step_anime = false 223 | @direction_fix = false 224 | @through = false 225 | @priority_type = 0 226 | @trigger = 0 227 | @list = [RPG::EventCommand.new] 228 | end 229 | attr_accessor :condition 230 | attr_accessor :graphic 231 | attr_accessor :move_type 232 | attr_accessor :move_speed 233 | attr_accessor :move_frequency 234 | attr_accessor :move_route 235 | attr_accessor :walk_anime 236 | attr_accessor :step_anime 237 | attr_accessor :direction_fix 238 | attr_accessor :through 239 | attr_accessor :priority_type 240 | attr_accessor :trigger 241 | attr_accessor :list 242 | end 243 | 244 | class RPG::Event::Page::Condition 245 | def initialize 246 | @switch1_valid = false 247 | @switch2_valid = false 248 | @variable_valid = false 249 | @self_switch_valid = false 250 | @item_valid = false 251 | @actor_valid = false 252 | @switch1_id = 1 253 | @switch2_id = 1 254 | @variable_id = 1 255 | @variable_value = 0 256 | @self_switch_ch = 'A' 257 | @item_id = 1 258 | @actor_id = 1 259 | end 260 | attr_accessor :switch1_valid 261 | attr_accessor :switch2_valid 262 | attr_accessor :variable_valid 263 | attr_accessor :self_switch_valid 264 | attr_accessor :item_valid 265 | attr_accessor :actor_valid 266 | attr_accessor :switch1_id 267 | attr_accessor :switch2_id 268 | attr_accessor :variable_id 269 | attr_accessor :variable_value 270 | attr_accessor :self_switch_ch 271 | attr_accessor :item_id 272 | attr_accessor :actor_id 273 | end 274 | 275 | class RPG::Event::Page::Graphic 276 | def initialize 277 | @tile_id = 0 278 | @character_name = '' 279 | @character_index = 0 280 | @direction = 2 281 | @pattern = 0 282 | end 283 | attr_accessor :tile_id 284 | attr_accessor :character_name 285 | attr_accessor :character_index 286 | attr_accessor :direction 287 | attr_accessor :pattern 288 | end 289 | 290 | class RPG::EventCommand 291 | def initialize(code = 0, indent = 0, parameters = []) 292 | @code = code 293 | @indent = indent 294 | @parameters = parameters 295 | end 296 | attr_accessor :code 297 | attr_accessor :indent 298 | attr_accessor :parameters 299 | end 300 | 301 | class RPG::MoveRoute 302 | def initialize 303 | @repeat = true 304 | @skippable = false 305 | @wait = false 306 | @list = [RPG::MoveCommand.new] 307 | end 308 | attr_accessor :repeat 309 | attr_accessor :skippable 310 | attr_accessor :wait 311 | attr_accessor :list 312 | end 313 | class RPG::MoveCommand 314 | def initialize(code = 0, parameters = []) 315 | @code = code 316 | @parameters = parameters 317 | end 318 | attr_accessor :code 319 | attr_accessor :parameters 320 | end 321 | 322 | class RPG::CommonEvent 323 | def initialize 324 | @id = 0 325 | @name = '' 326 | @trigger = 0 327 | @switch_id = 1 328 | @list = [RPG::EventCommand.new] 329 | end 330 | def autorun? 331 | @trigger == 1 332 | end 333 | def parallel? 334 | @trigger == 2 335 | end 336 | attr_accessor :id 337 | attr_accessor :name 338 | attr_accessor :trigger 339 | attr_accessor :switch_id 340 | attr_accessor :list 341 | end 342 | class RPG::BaseItem 343 | def initialize 344 | @id = 0 345 | @name = '' 346 | @icon_index = 0 347 | @description = '' 348 | @features = [] 349 | @note = '' 350 | end 351 | attr_accessor :id 352 | attr_accessor :name 353 | attr_accessor :icon_index 354 | attr_accessor :description 355 | attr_accessor :features 356 | attr_accessor :note 357 | end 358 | 359 | class RPG::BaseItem::Feature 360 | def initialize(code = 0, data_id = 0, value = 0) 361 | @code = code 362 | @data_id = data_id 363 | @value = value 364 | end 365 | attr_accessor :code 366 | attr_accessor :data_id 367 | attr_accessor :value 368 | end 369 | 370 | class RPG::Actor < RPG::BaseItem 371 | def initialize 372 | super 373 | @nickname = '' 374 | @class_id = 1 375 | @initial_level = 1 376 | @max_level = 99 377 | @character_name = '' 378 | @character_index = 0 379 | @face_name = '' 380 | @face_index = 0 381 | @equips = [0, 0, 0, 0, 0] 382 | end 383 | attr_accessor :nickname 384 | attr_accessor :class_id 385 | attr_accessor :initial_level 386 | attr_accessor :max_level 387 | attr_accessor :character_name 388 | attr_accessor :character_index 389 | attr_accessor :face_name 390 | attr_accessor :face_index 391 | attr_accessor :equips 392 | end 393 | class RPG::Class < RPG::BaseItem 394 | def initialize 395 | super 396 | @exp_params = [30, 20, 30, 30] 397 | @params = Table.new(8, 100) 398 | (1..99).each do |i| 399 | @params[0, i] = 400 + i * 50 400 | @params[1, i] = 80 + i * 10 401 | (2..5).each { |j| @params[j, i] = 15 + i * 5 / 4 } 402 | (6..7).each { |j| @params[j, i] = 30 + i * 5 / 2 } 403 | end 404 | @learnings = [] 405 | @features.push(RPG::BaseItem::Feature.new(23, 0, 1)) 406 | @features.push(RPG::BaseItem::Feature.new(22, 0, 0.95)) 407 | @features.push(RPG::BaseItem::Feature.new(22, 1, 0.05)) 408 | @features.push(RPG::BaseItem::Feature.new(22, 2, 0.04)) 409 | @features.push(RPG::BaseItem::Feature.new(41, 1)) 410 | @features.push(RPG::BaseItem::Feature.new(51, 1)) 411 | @features.push(RPG::BaseItem::Feature.new(52, 1)) 412 | end 413 | def exp_for_level(level) 414 | lv = level.to_f 415 | basis = @exp_params[0].to_f 416 | extra = @exp_params[1].to_f 417 | acc_a = @exp_params[2].to_f 418 | acc_b = @exp_params[3].to_f 419 | return( 420 | ( 421 | basis * ((lv - 1)**(0.9 + acc_a / 250)) * lv * (lv + 1) / 422 | (6 + lv**2 / 50 / acc_b) + (lv - 1) * extra 423 | ).round.to_i 424 | ) 425 | end 426 | attr_accessor :exp_params 427 | attr_accessor :params 428 | attr_accessor :learnings 429 | end 430 | 431 | class RPG::Class::Learning 432 | def initialize 433 | @level = 1 434 | @skill_id = 1 435 | @note = '' 436 | end 437 | attr_accessor :level 438 | attr_accessor :skill_id 439 | attr_accessor :note 440 | end 441 | 442 | class RPG::UsableItem < RPG::BaseItem 443 | def initialize 444 | super 445 | @scope = 0 446 | @occasion = 0 447 | @speed = 0 448 | @success_rate = 100 449 | @repeats = 1 450 | @tp_gain = 0 451 | @hit_type = 0 452 | @animation_id = 0 453 | @damage = RPG::UsableItem::Damage.new 454 | @effects = [] 455 | end 456 | def for_opponent? 457 | [1, 2, 3, 4, 5, 6].include?(@scope) 458 | end 459 | def for_friend? 460 | [7, 8, 9, 10, 11].include?(@scope) 461 | end 462 | def for_dead_friend? 463 | [9, 10].include?(@scope) 464 | end 465 | def for_user? 466 | @scope == 11 467 | end 468 | def for_one? 469 | [1, 3, 7, 9, 11].include?(@scope) 470 | end 471 | def for_random? 472 | [3, 4, 5, 6].include?(@scope) 473 | end 474 | def number_of_targets 475 | for_random? ? @scope - 2 : 0 476 | end 477 | def for_all? 478 | [2, 8, 10].include?(@scope) 479 | end 480 | def need_selection? 481 | [1, 7, 9].include?(@scope) 482 | end 483 | def battle_ok? 484 | [0, 1].include?(@occasion) 485 | end 486 | def menu_ok? 487 | [0, 2].include?(@occasion) 488 | end 489 | def certain? 490 | @hit_type == 0 491 | end 492 | def physical? 493 | @hit_type == 1 494 | end 495 | def magical? 496 | @hit_type == 2 497 | end 498 | attr_accessor :scope 499 | attr_accessor :occasion 500 | attr_accessor :speed 501 | attr_accessor :animation_id 502 | attr_accessor :success_rate 503 | attr_accessor :repeats 504 | attr_accessor :tp_gain 505 | attr_accessor :hit_type 506 | attr_accessor :damage 507 | attr_accessor :effects 508 | end 509 | 510 | class RPG::UsableItem::Damage 511 | def initialize 512 | @type = 0 513 | @element_id = 0 514 | @formula = '0' 515 | @variance = 20 516 | @critical = false 517 | end 518 | def none? 519 | @type == 0 520 | end 521 | def to_hp? 522 | [1, 3, 5].include?(@type) 523 | end 524 | def to_mp? 525 | [2, 4, 6].include?(@type) 526 | end 527 | def recover? 528 | [3, 4].include?(@type) 529 | end 530 | def drain? 531 | [5, 6].include?(@type) 532 | end 533 | def sign 534 | recover? ? -1 : 1 535 | end 536 | def eval(a, b, v) 537 | begin 538 | [Kernel.eval(@formula), 0].max * sign 539 | rescue StandardError 540 | 0 541 | end 542 | end 543 | attr_accessor :type 544 | attr_accessor :element_id 545 | attr_accessor :formula 546 | attr_accessor :variance 547 | attr_accessor :critical 548 | end 549 | 550 | class RPG::UsableItem::Effect 551 | def initialize(code = 0, data_id = 0, value1 = 0, value2 = 0) 552 | @code = code 553 | @data_id = data_id 554 | @value1 = value1 555 | @value2 = value2 556 | end 557 | attr_accessor :code 558 | attr_accessor :data_id 559 | attr_accessor :value1 560 | attr_accessor :value2 561 | end 562 | 563 | class RPG::Skill < RPG::UsableItem 564 | def initialize 565 | super 566 | @scope = 1 567 | @stype_id = 1 568 | @mp_cost = 0 569 | @tp_cost = 0 570 | @message1 = '' 571 | @message2 = '' 572 | @required_wtype_id1 = 0 573 | @required_wtype_id2 = 0 574 | end 575 | attr_accessor :stype_id 576 | attr_accessor :mp_cost 577 | attr_accessor :tp_cost 578 | attr_accessor :message1 579 | attr_accessor :message2 580 | attr_accessor :required_wtype_id1 581 | attr_accessor :required_wtype_id2 582 | end 583 | class RPG::Item < RPG::UsableItem 584 | def initialize 585 | super 586 | @scope = 7 587 | @itype_id = 1 588 | @price = 0 589 | @consumable = true 590 | end 591 | def key_item? 592 | @itype_id == 2 593 | end 594 | attr_accessor :itype_id 595 | attr_accessor :price 596 | attr_accessor :consumable 597 | end 598 | 599 | class RPG::EquipItem < RPG::BaseItem 600 | def initialize 601 | super 602 | @price = 0 603 | @etype_id = 0 604 | @params = [0] * 8 605 | end 606 | attr_accessor :price 607 | attr_accessor :etype_id 608 | attr_accessor :params 609 | end 610 | class RPG::Weapon < RPG::EquipItem 611 | def initialize 612 | super 613 | @wtype_id = 0 614 | @animation_id = 0 615 | @features.push(RPG::BaseItem::Feature.new(31, 1, 0)) 616 | @features.push(RPG::BaseItem::Feature.new(22, 0, 0)) 617 | end 618 | def performance 619 | params[2] + params[4] + params.inject(0) { |r, v| r += v } 620 | end 621 | attr_accessor :wtype_id 622 | attr_accessor :animation_id 623 | end 624 | 625 | class RPG::Armor < RPG::EquipItem 626 | def initialize 627 | super 628 | @atype_id = 0 629 | @etype_id = 1 630 | @features.push(RPG::BaseItem::Feature.new(22, 1, 0)) 631 | end 632 | def performance 633 | params[3] + params[5] + params.inject(0) { |r, v| r += v } 634 | end 635 | attr_accessor :atype_id 636 | end 637 | 638 | class RPG::Enemy < RPG::BaseItem 639 | def initialize 640 | super 641 | @battler_name = '' 642 | @battler_hue = 0 643 | @params = [100, 0, 10, 10, 10, 10, 10, 10] 644 | @exp = 0 645 | @gold = 0 646 | @drop_items = Array.new(3) { RPG::Enemy::DropItem.new } 647 | @actions = [RPG::Enemy::Action.new] 648 | @features.push(RPG::BaseItem::Feature.new(22, 0, 0.95)) 649 | @features.push(RPG::BaseItem::Feature.new(22, 1, 0.05)) 650 | @features.push(RPG::BaseItem::Feature.new(31, 1, 0)) 651 | end 652 | attr_accessor :battler_name 653 | attr_accessor :battler_hue 654 | attr_accessor :params 655 | attr_accessor :exp 656 | attr_accessor :gold 657 | attr_accessor :drop_items 658 | attr_accessor :actions 659 | end 660 | 661 | class RPG::Enemy::DropItem 662 | def initialize 663 | @kind = 0 664 | @data_id = 1 665 | @denominator = 1 666 | end 667 | attr_accessor :kind 668 | attr_accessor :data_id 669 | attr_accessor :denominator 670 | end 671 | 672 | class RPG::Enemy::Action 673 | def initialize 674 | @skill_id = 1 675 | @condition_type = 0 676 | @condition_param1 = 0 677 | @condition_param2 = 0 678 | @rating = 5 679 | end 680 | attr_accessor :skill_id 681 | attr_accessor :condition_type 682 | attr_accessor :condition_param1 683 | attr_accessor :condition_param2 684 | attr_accessor :rating 685 | end 686 | 687 | class RPG::State < RPG::BaseItem 688 | def initialize 689 | super 690 | @restriction = 0 691 | @priority = 50 692 | @remove_at_battle_end = false 693 | @remove_by_restriction = false 694 | @auto_removal_timing = 0 695 | @min_turns = 1 696 | @max_turns = 1 697 | @remove_by_damage = false 698 | @chance_by_damage = 100 699 | @remove_by_walking = false 700 | @steps_to_remove = 100 701 | @message1 = '' 702 | @message2 = '' 703 | @message3 = '' 704 | @message4 = '' 705 | end 706 | attr_accessor :restriction 707 | attr_accessor :priority 708 | attr_accessor :remove_at_battle_end 709 | attr_accessor :remove_by_restriction 710 | attr_accessor :auto_removal_timing 711 | attr_accessor :min_turns 712 | attr_accessor :max_turns 713 | attr_accessor :remove_by_damage 714 | attr_accessor :chance_by_damage 715 | attr_accessor :remove_by_walking 716 | attr_accessor :steps_to_remove 717 | attr_accessor :message1 718 | attr_accessor :message2 719 | attr_accessor :message3 720 | attr_accessor :message4 721 | end 722 | 723 | class RPG::Troop 724 | def initialize 725 | @id = 0 726 | @name = '' 727 | @members = [] 728 | @pages = [RPG::Troop::Page.new] 729 | end 730 | attr_accessor :id 731 | attr_accessor :name 732 | attr_accessor :members 733 | attr_accessor :pages 734 | end 735 | 736 | class RPG::Troop::Member 737 | def initialize 738 | @enemy_id = 1 739 | @x = 0 740 | @y = 0 741 | @hidden = false 742 | end 743 | attr_accessor :enemy_id 744 | attr_accessor :x 745 | attr_accessor :y 746 | attr_accessor :hidden 747 | end 748 | 749 | class RPG::Troop::Page 750 | def initialize 751 | @condition = RPG::Troop::Page::Condition.new 752 | @span = 0 753 | @list = [RPG::EventCommand.new] 754 | end 755 | attr_accessor :condition 756 | attr_accessor :span 757 | attr_accessor :list 758 | end 759 | 760 | class RPG::Troop::Page::Condition 761 | def initialize 762 | @turn_ending = false 763 | @turn_valid = false 764 | @enemy_valid = false 765 | @actor_valid = false 766 | @switch_valid = false 767 | @turn_a = 0 768 | @turn_b = 0 769 | @enemy_index = 0 770 | @enemy_hp = 50 771 | @actor_id = 1 772 | @actor_hp = 50 773 | @switch_id = 1 774 | end 775 | attr_accessor :turn_ending 776 | attr_accessor :turn_valid 777 | attr_accessor :enemy_valid 778 | attr_accessor :actor_valid 779 | attr_accessor :switch_valid 780 | attr_accessor :turn_a 781 | attr_accessor :turn_b 782 | attr_accessor :enemy_index 783 | attr_accessor :enemy_hp 784 | attr_accessor :actor_id 785 | attr_accessor :actor_hp 786 | attr_accessor :switch_id 787 | end 788 | 789 | class RPG::Animation 790 | def initialize 791 | @id = 0 792 | @name = '' 793 | @animation1_name = '' 794 | @animation1_hue = 0 795 | @animation2_name = '' 796 | @animation2_hue = 0 797 | @position = 1 798 | @frame_max = 1 799 | @frames = [RPG::Animation::Frame.new] 800 | @timings = [] 801 | end 802 | def to_screen? 803 | @position == 3 804 | end 805 | attr_accessor :id 806 | attr_accessor :name 807 | attr_accessor :animation1_name 808 | attr_accessor :animation1_hue 809 | attr_accessor :animation2_name 810 | attr_accessor :animation2_hue 811 | attr_accessor :position 812 | attr_accessor :frame_max 813 | attr_accessor :frames 814 | attr_accessor :timings 815 | end 816 | 817 | class RPG::Animation::Frame 818 | def initialize 819 | @cell_max = 0 820 | @cell_data = Table.new(0, 0) 821 | end 822 | attr_accessor :cell_max 823 | attr_accessor :cell_data 824 | end 825 | 826 | class RPG::Animation::Timing 827 | def initialize 828 | @frame = 0 829 | @se = RPG::SE.new('', 80) 830 | @flash_scope = 0 831 | @flash_color = Color.new(255, 255, 255, 255) 832 | @flash_duration = 5 833 | end 834 | attr_accessor :frame 835 | attr_accessor :se 836 | attr_accessor :flash_scope 837 | attr_accessor :flash_color 838 | attr_accessor :flash_duration 839 | end 840 | 841 | class RPG::System 842 | def initialize 843 | @game_title = '' 844 | @version_id = 0 845 | @japanese = true 846 | @party_members = [1] 847 | @currency_unit = '' 848 | @elements = [nil, ''] 849 | @skill_types = [nil, ''] 850 | @weapon_types = [nil, ''] 851 | @armor_types = [nil, ''] 852 | @switches = [nil, ''] 853 | @variables = [nil, ''] 854 | @boat = RPG::System::Vehicle.new 855 | @ship = RPG::System::Vehicle.new 856 | @airship = RPG::System::Vehicle.new 857 | @title1_name = '' 858 | @title2_name = '' 859 | @opt_draw_title = true 860 | @opt_use_midi = false 861 | @opt_transparent = false 862 | @opt_followers = true 863 | @opt_slip_death = false 864 | @opt_floor_death = false 865 | @opt_display_tp = true 866 | @opt_extra_exp = false 867 | @window_tone = Tone.new(0, 0, 0) 868 | @title_bgm = RPG::BGM.new 869 | @battle_bgm = RPG::BGM.new 870 | @battle_end_me = RPG::ME.new 871 | @gameover_me = RPG::ME.new 872 | @sounds = Array.new(24) { RPG::SE.new } 873 | @test_battlers = [] 874 | @test_troop_id = 1 875 | @start_map_id = 1 876 | @start_x = 0 877 | @start_y = 0 878 | @terms = RPG::System::Terms.new 879 | @battleback1_name = '' 880 | @battleback2_name = '' 881 | @battler_name = '' 882 | @battler_hue = 0 883 | @edit_map_id = 1 884 | end 885 | attr_accessor :game_title 886 | attr_accessor :version_id 887 | attr_accessor :japanese 888 | attr_accessor :party_members 889 | attr_accessor :currency_unit 890 | attr_accessor :skill_types 891 | attr_accessor :weapon_types 892 | attr_accessor :armor_types 893 | attr_accessor :elements 894 | attr_accessor :switches 895 | attr_accessor :variables 896 | attr_accessor :boat 897 | attr_accessor :ship 898 | attr_accessor :airship 899 | attr_accessor :title1_name 900 | attr_accessor :title2_name 901 | attr_accessor :opt_draw_title 902 | attr_accessor :opt_use_midi 903 | attr_accessor :opt_transparent 904 | attr_accessor :opt_followers 905 | attr_accessor :opt_slip_death 906 | attr_accessor :opt_floor_death 907 | attr_accessor :opt_display_tp 908 | attr_accessor :opt_extra_exp 909 | attr_accessor :window_tone 910 | attr_accessor :title_bgm 911 | attr_accessor :battle_bgm 912 | attr_accessor :battle_end_me 913 | attr_accessor :gameover_me 914 | attr_accessor :sounds 915 | attr_accessor :test_battlers 916 | attr_accessor :test_troop_id 917 | attr_accessor :start_map_id 918 | attr_accessor :start_x 919 | attr_accessor :start_y 920 | attr_accessor :terms 921 | attr_accessor :battleback1_name 922 | attr_accessor :battleback2_name 923 | attr_accessor :battler_name 924 | attr_accessor :battler_hue 925 | attr_accessor :edit_map_id 926 | end 927 | 928 | class RPG::System::Vehicle 929 | def initialize 930 | @character_name = '' 931 | @character_index = 0 932 | @bgm = RPG::BGM.new 933 | @start_map_id = 0 934 | @start_x = 0 935 | @start_y = 0 936 | end 937 | attr_accessor :character_name 938 | attr_accessor :character_index 939 | attr_accessor :bgm 940 | attr_accessor :start_map_id 941 | attr_accessor :start_x 942 | attr_accessor :start_y 943 | end 944 | 945 | class RPG::System::Terms 946 | def initialize 947 | @basic = Array.new(8) { '' } 948 | @params = Array.new(8) { '' } 949 | @etypes = Array.new(5) { '' } 950 | @commands = Array.new(23) { '' } 951 | end 952 | attr_accessor :basic 953 | attr_accessor :params 954 | attr_accessor :etypes 955 | attr_accessor :commands 956 | end 957 | 958 | class RPG::System::TestBattler 959 | def initialize 960 | @actor_id = 1 961 | @level = 1 962 | @equips = [0, 0, 0, 0, 0] 963 | end 964 | attr_accessor :actor_id 965 | attr_accessor :level 966 | attr_accessor :equips 967 | end 968 | -------------------------------------------------------------------------------- /RGSS3/modules/Table.rb: -------------------------------------------------------------------------------- 1 | ## ======================================== 2 | ## Original Source 3 | ## https://github.com/CaptainJet/RM-Gosu 4 | ## ======================================== 5 | class Table 6 | attr_accessor :xsize, :ysize, :zsize, :data 7 | 8 | def initialize(x, y = 0, z = 0) 9 | @dim = 1 + (y > 0 ? 1 : 0) + (z > 0 ? 1 : 0) 10 | @xsize, @ysize, @zsize = x, [y, 1].max, [z, 1].max 11 | @data = Array.new(x * y * z, 0) 12 | end 13 | 14 | def [](x, y = 0, z = 0) 15 | @data[x + y * @xsize + z * @xsize * @ysize] 16 | end 17 | 18 | def resize(x, y = nil, z = nil) 19 | @dim = 1 + ((y || @ysize) > 0 ? 1 : 0) + ((z || @zsize) > 0 ? 1 : 0) 20 | @xsize, @ysize, @zsize = x, [y || @ysize, 1].max, [z || @zsize, 1].max 21 | @data = @data[0, @xsize * @ysize * @zsize - 1] 22 | @data << 0 until @data.size == @xsize * @ysize * @zsize 23 | end 24 | 25 | def []=(*args) 26 | x = args[0] 27 | y = args.size > 2 ? args[1] : 0 28 | z = args.size > 3 ? args[2] : 0 29 | v = args.pop 30 | @data[x + y * @xsize + z * @xsize * @ysize] = v 31 | end 32 | 33 | def _dump(d = 0) 34 | s = 35 | [@dim, @xsize, @ysize, @zsize, @xsize * @ysize * @zsize].pack( 36 | 'LLLLL', 37 | ) 38 | a = [] 39 | ta = [] 40 | @data.each do |d| 41 | if d.is_a?(Fixnum) && (d < 32_768 && d >= 0) 42 | s << [d].pack('S') 43 | else 44 | s << [ta].pack("S#{ta.size}") 45 | ni = a.size 46 | a << d 47 | s << [0x8000 | ni].pack('S') 48 | end 49 | end 50 | s << Marshal.dump(a) if a.size > 0 51 | s 52 | end 53 | 54 | def self._load(s) 55 | size, nx, ny, nz, items = *s[0, 20].unpack('LLLLL') 56 | t = Table.new(*[nx, ny, nz][0, size]) 57 | d = s[20, items * 2].unpack("S#{items}") 58 | if s.length > (20 + items * 2) 59 | a = Marshal.load(s[(20 + items * 2)...s.length]) 60 | d.collect! { |i| i & 0x8000 == 0x8000 ? a[i & ~0x8000] : i } 61 | end 62 | t.data = d 63 | t 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /RGSS3/plugins/rxscript.rb: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # ** rxscript io (1.0) 3 | #------------------------------------------------------------------------------ 4 | # Original Author : Korokke (gksdntjr714@naver.com) 5 | # Created : 2019-02-10 6 | # Latest : 2019-02-10 7 | # Original Source : https://cafe.naver.com/xpcafe/172565 8 | # Example : 9 | # begin 10 | # RXDATA.ExtractScript("Extracted", "Data\\Scripts.rxdata") 11 | # RXDATA.CompressScript("Extracted", "Data\\test.rxdata") 12 | # RXDATA.ExtractScript("Extracted-test", "Data\\test.rxdata") 13 | # end 14 | #------------------------------------------------------------------------------ 15 | # Modified by : Jinseok Eo 16 | # - Fixed an issue where filenames were always set to ASCII-8BIT. 17 | #============================================================================== 18 | 19 | require 'securerandom' 20 | require 'json' 21 | 22 | module UUID 23 | def self.v4 24 | SecureRandom.uuid 25 | end 26 | end 27 | 28 | class ScriptIdentifier 29 | attr_reader :uuid, :name, :index 30 | 31 | def initialize(index, name) 32 | @uuid = UUID.v4 33 | @name = name 34 | @index = index 35 | end 36 | 37 | def to_s 38 | { uuid: @uuid, name: @name, index: @index }.to_json 39 | end 40 | end 41 | 42 | module RXDATA 43 | @@usedSections = [] 44 | 45 | module_function 46 | 47 | def GetRandomSection 48 | begin 49 | section = rand(2_147_483_647) 50 | end while @@usedSections.include?(section) 51 | @@usedSections.push(section) 52 | return section 53 | end 54 | 55 | def ZlibInflate(str) 56 | zstream = ::Zlib::Inflate.new 57 | buf = zstream.inflate(str) 58 | zstream.finish 59 | zstream.close 60 | return buf 61 | end 62 | 63 | def ZlibDeflate(str) 64 | z = ::Zlib::Deflate.new(::Zlib::BEST_COMPRESSION) 65 | dst = z.deflate(str, ::Zlib::FINISH) 66 | z.close 67 | return dst 68 | end 69 | 70 | class Script 71 | attr_reader :section 72 | attr_reader :title 73 | attr_reader :text 74 | 75 | def initialize(section, title, text) 76 | @section = section 77 | @title = title.to_s 78 | @text = RXDATA.ZlibInflate(text) 79 | end 80 | 81 | def rmscript 82 | return @section, @title, RXDATA.ZlibDeflate(@text) 83 | end 84 | end 85 | 86 | def ExtractScript(outdir, rxdata) 87 | return unless File.exist? rxdata 88 | Dir.mkdir(outdir) unless File.exist? outdir 89 | 90 | input = File.open(rxdata, 'rb') 91 | scripts = Marshal.load(input.read) 92 | info = '' 93 | 94 | # ScriptIdentifier[] 95 | script_identifier = {} 96 | script_index = 0 97 | 98 | for script in scripts 99 | if script.length == 1 100 | tmp = script[0] 101 | tmp[0] = RXDATA.GetRandomSection 102 | data = Script.new(tmp[0], tmp[1], tmp[2]) 103 | elsif script.length == 2 104 | data = Script.new(RXDATA.GetRandomSection, script[0], script[1]) 105 | elsif script.length == 3 106 | data = Script.new(RXDATA.GetRandomSection, script[1], script[2]) 107 | end 108 | 109 | identifier = ScriptIdentifier.new(script_index, data.title) 110 | script_identifier[identifier.uuid] = identifier 111 | 112 | # add index prefix like as "001" to title 113 | prefix = script_index.to_s.rjust(3, '0') 114 | 115 | title = 116 | if data.title.empty? 117 | "#{prefix}-Untitled" 118 | else 119 | "#{prefix}-#{data.title}" 120 | end 121 | 122 | filename = title + '.rb' 123 | info += filename + "\n" 124 | output = File.new(File.join(outdir, filename), 'wb') 125 | output.write(data.text) # "\n"을 replace 하는 코드 제거 126 | output.close 127 | 128 | script_index += 1 129 | end 130 | 131 | # txt 132 | output = File.new(File.join(outdir, 'info.txt'), 'wb') 133 | output.write(info) 134 | output.close 135 | 136 | input.close 137 | end 138 | 139 | def CompressScript(indir, rxdata) 140 | return unless File.exist? indir 141 | 142 | files = [] 143 | if File.exist? File.join(indir, 'info.txt') 144 | input = File.open(File.join(indir, 'info.txt'), 'rb') 145 | input.read.each_line do |line| 146 | filename = line.gsub("\n", '') # it is always ASCII-8BIT 147 | filename.force_encoding('utf-8') 148 | 149 | files.push(File.join(indir, filename)) 150 | end 151 | input.close 152 | else 153 | files = Dir.glob(File.join(indir, '*.rb')) 154 | end 155 | 156 | scripts = [] 157 | for rb in files 158 | input = File.open(rb, 'r') 159 | section = RXDATA.GetRandomSection 160 | title = File.basename(rb, '.rb') 161 | 162 | find_line_regexp = /^[\d]{3}\-[.]*/i 163 | if title.start_with?(find_line_regexp) 164 | title = title.gsub!(find_line_regexp, '') 165 | end 166 | 167 | title = '' if title =~ /^(?:Untitled)$/ 168 | text = input.read 169 | text = text.force_encoding('utf-8') 170 | text = RXDATA.ZlibDeflate(text) 171 | scripts.push([section, title, text]) 172 | input.close 173 | end 174 | save = Marshal.dump(scripts) 175 | 176 | output = File.new(rxdata, 'wb+') 177 | output.write(save) 178 | output.close 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /RGSS3/test/hangul.test.rb: -------------------------------------------------------------------------------- 1 | 2 | # The current extension path is c:\Users\biud4\.vscode\extensions\biud436.rgss-script-compiler-0.9.2. 3 | # [file changed] {"$mid":1,"fsPath":"c:\\Users\\biud4\\OneDrive\\문서\\RPGVXAce\\VXA\\Scripts\\121-RS_GlareEffect.rb","_sep":1,"path":"/c:/Users/biud4/OneDrive/문서/RPGVXAce/VXA/Scripts/121-RS_GlareEffect.rb","scheme":"file"} 4 | # incompatible character encodings: UTF-8 and ASCII-8BIT 5 | 6 | file_path = "C:\\Users\\biud4\\OneDrive\\문서\\RPGVXAce\\VXA\\Game.exe".encoding 7 | 8 | p file_path -------------------------------------------------------------------------------- /images/dep.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slice 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /images/icon-simple256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/images/icon-simple256x256.png -------------------------------------------------------------------------------- /images/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/images/icon128x128.png -------------------------------------------------------------------------------- /images/icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/images/icon256x256.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Automatically clear mock calls, instances, contexts and results before every test 3 | clearMocks: true, 4 | 5 | // Indicates whether the coverage information should be collected while executing the test 6 | collectCoverage: false, 7 | 8 | // The directory where Jest should output its coverage files 9 | coverageDirectory: "coverage", 10 | 11 | // Indicates which provider should be used to instrument code for coverage 12 | coverageProvider: "v8", 13 | 14 | // An array of file extensions your modules use 15 | moduleFileExtensions: [ 16 | "js", 17 | "mjs", 18 | "cjs", 19 | "jsx", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "node", 24 | ], 25 | 26 | transform: { 27 | "^.+\\.(t|j)s$": "ts-jest", 28 | }, 29 | 30 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 31 | moduleNameMapper: {}, 32 | 33 | // The test environment that will be used for testing 34 | testEnvironment: "jest-environment-node", 35 | 36 | // The glob patterns Jest uses to detect test files 37 | testMatch: [ 38 | "/**/*.test.(js|jsx|ts|tsx)", 39 | "/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))", 40 | ], 41 | 42 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 43 | transformIgnorePatterns: ["/node_modules/"], 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgss-script-compiler", 3 | "displayName": "RGSS Script Compiler", 4 | "description": "This extension allows you to compile script files as a bundle file called 'Scripts.rvdata2'", 5 | "repository": "https://github.com/biud436/vscode-rgss-script-compiler", 6 | "version": "1.0.0", 7 | "engines": { 8 | "vscode": "^1.81.0" 9 | }, 10 | "publisher": "biud436", 11 | "license": "MIT", 12 | "categories": [ 13 | "Other" 14 | ], 15 | "icon": "images/icon-simple256x256.png", 16 | "activationEvents": [ 17 | "onCommand:rgss-script-compiler.setGamePath", 18 | "onCommand:rgss-script-compiler.unpack", 19 | "onCommand:rgss-script-compiler.compile", 20 | "workspaceContains:**/Game.ini", 21 | "onView:rgssScriptViewer" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/biud436/vscode-rgss-script-compiler/issues", 25 | "email": "biud436@gmail.com" 26 | }, 27 | "keywords": [ 28 | "rgss", 29 | "rpg maker vx ace", 30 | "rpg maker xp" 31 | ], 32 | "main": "./out/extension.js", 33 | "contributes": { 34 | "commands": [ 35 | { 36 | "command": "rgss-script-compiler.setGamePath", 37 | "title": "Set Game Path", 38 | "category": "RGSS Compiler", 39 | "icon": "$(file-directory)" 40 | }, 41 | { 42 | "command": "rgss-script-compiler.unpack", 43 | "title": "Unpack", 44 | "category": "RGSS Compiler", 45 | "icon": "$(package)" 46 | }, 47 | { 48 | "command": "rgss-script-compiler.compile", 49 | "title": "Compile", 50 | "category": "RGSS Compiler", 51 | "icon": "$(file-binary)" 52 | }, 53 | { 54 | "command": "rgss-script-compiler.testPlay", 55 | "title": "Test Play", 56 | "category": "RGSS Compiler", 57 | "icon": "$(debug-console)" 58 | }, 59 | { 60 | "command": "rgss-script-compiler.save", 61 | "title": "Save", 62 | "category": "RGSS Compiler" 63 | }, 64 | { 65 | "command": "rgss-script-compiler.importAuto", 66 | "title": "Import Auto", 67 | "category": "RGSS Compiler" 68 | }, 69 | { 70 | "command": "rgssScriptViewer.refreshEntry", 71 | "category": "RGSS Compiler", 72 | "title": "Refresh" 73 | }, 74 | { 75 | "command": "rgss-script-compiler.openGameFolder", 76 | "title": "Open Game Folder", 77 | "category": "RGSS Compiler", 78 | "icon": "$(root-folder-opened)" 79 | }, 80 | { 81 | "command": "rgss-script-compiler.openScript", 82 | "title": "Open Script", 83 | "category": "RGSS Compiler" 84 | }, 85 | { 86 | "command": "rgss-script-compiler.newFile", 87 | "title": "New File", 88 | "category": "RGSS Compiler", 89 | "icon": "$(file-add)" 90 | }, 91 | { 92 | "command": "rgss-script-compiler.deleteFile", 93 | "title": "Delete", 94 | "category": "RGSS Compiler", 95 | "icon": "$(trash)" 96 | }, 97 | { 98 | "command": "rgss-script-compiler.renameFile", 99 | "title": "Rename", 100 | "category": "RGSS Compiler", 101 | "icon": "$(edit-rename)" 102 | }, 103 | { 104 | "command": "rgss-script-compiler.refreshScriptExplorer", 105 | "title": "Refresh", 106 | "category": "RGSS Compiler", 107 | "icon": "$(refresh)" 108 | } 109 | ], 110 | "keybindings": [ 111 | { 112 | "command": "rgss-script-compiler.save", 113 | "key": "ctrl+s", 114 | "mac": "cmd+s", 115 | "when": "editorTextFocus && resourceExtname == .rb" 116 | }, 117 | { 118 | "command": "rgss-script-compiler.testPlay", 119 | "key": "f5", 120 | "when": "editorTextFocus && resourceExtname == .rb", 121 | "mac": "f5" 122 | } 123 | ], 124 | "viewsContainers": { 125 | "activitybar": [ 126 | { 127 | "id": "rgssScriptViewer", 128 | "title": "Script Editor", 129 | "icon": "images/dep.svg" 130 | } 131 | ] 132 | }, 133 | "views": { 134 | "rgssScriptViewer": [ 135 | { 136 | "id": "rgssScriptViewer", 137 | "name": "Script Editor", 138 | "icon": "images/dep.svg", 139 | "contextualTitle": "Script Editor" 140 | } 141 | ] 142 | }, 143 | "menus": { 144 | "view/title": [ 145 | { 146 | "command": "rgss-script-compiler.setGamePath", 147 | "when": "view == rgssScriptViewer", 148 | "group": "navigation" 149 | }, 150 | { 151 | "command": "rgss-script-compiler.unpack", 152 | "when": "view == rgssScriptViewer", 153 | "group": "navigation" 154 | }, 155 | { 156 | "command": "rgss-script-compiler.refreshScriptExplorer", 157 | "when": "view == rgssScriptViewer", 158 | "group": "navigation" 159 | } 160 | ], 161 | "view/item/context": [ 162 | { 163 | "command": "rgss-script-compiler.newFile", 164 | "when": "view == rgssScriptViewer", 165 | "group": "inline" 166 | }, 167 | { 168 | "command": "rgss-script-compiler.deleteFile", 169 | "when": "view == rgssScriptViewer", 170 | "group": "inline" 171 | }, 172 | { 173 | "command": "rgss-script-compiler.renameFile", 174 | "when": "view == rgssScriptViewer" 175 | } 176 | ] 177 | }, 178 | "configuration": [ 179 | { 180 | "id": "rgssScriptCompiler", 181 | "title": "RGSS Script Compiler", 182 | "properties": { 183 | "rgssScriptCompiler.showStatusBar": { 184 | "type": "boolean", 185 | "default": true, 186 | "description": "Show Status Bar" 187 | }, 188 | "rgssScriptCompiler.macOsGamePath": { 189 | "type": "string", 190 | "default": "", 191 | "description": "MacOS MKXP-Z Game Directory" 192 | }, 193 | "rgssScriptCompiler.macOsBundleIdentifier": { 194 | "type": "string", 195 | "default": "org.struma.mkxp-z", 196 | "description": "MacOS MKXP-Z Bundle Identifier" 197 | } 198 | } 199 | } 200 | ], 201 | "viewsWelcome": [ 202 | { 203 | "view": "rgssScriptViewer", 204 | "contents": "Welcome to the Script Editor. You can use this extension to edit the script files of RPG Maker XP, VX, and VX Ace.\n\n[Import Project](command:rgss-script-compiler.importAuto)\n\n" 205 | } 206 | ] 207 | }, 208 | "scripts": { 209 | "vscode:prepublish": "yarn run compile", 210 | "compile": "tsc -p ./", 211 | "watch": "tsc -watch -p ./", 212 | "pretest": "yarn run compile && yarn run lint", 213 | "lint": "eslint src --ext ts", 214 | "vsc:test": "node ./out/test/runTest.js", 215 | "publish": "vsce package" 216 | }, 217 | "devDependencies": { 218 | "@prettier/plugin-ruby": "^4.0.2", 219 | "@types/glob": "^8.1.0", 220 | "@types/node": "20.x", 221 | "@types/vscode": "^1.81.0", 222 | "@typescript-eslint/eslint-plugin": "^6.3.0", 223 | "@typescript-eslint/parser": "^6.3.0", 224 | "@vscode/test-electron": "^2.3.4", 225 | "babel-jest": "^29.7.0", 226 | "eslint": "^8.46.0", 227 | "glob": "^10.3.3", 228 | "prettier": "^3.0.1", 229 | "reflect-metadata": "^0.2.2", 230 | "typescript": "^5.1.6" 231 | }, 232 | "dependencies": { 233 | "@hyrious/marshal": "^0.3.3", 234 | "@types/marshal": "^0.5.3", 235 | "@types/uuid": "^10.0.0", 236 | "chalk": "^4", 237 | "dayjs": "^1.11.13", 238 | "marshal": "^0.5.4", 239 | "uuid": "^11.0.3" 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/Helper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from "vscode"; 3 | import { Path } from "./utils/Path"; 4 | import { setGamePath } from "./commands/SetGamePath"; 5 | import { ConfigService } from "./services/ConfigService"; 6 | import { LoggingService } from "./services/LoggingService"; 7 | import { Packer } from "./Packer"; 8 | import { Unpacker } from "./Unpacker"; 9 | import { openGameFolder } from "./commands/OpenGameFolder"; 10 | import { GamePlayService } from "./commands/TestGamePlay"; 11 | import { ScriptExplorerProvider } from "./providers/ScriptViewer"; 12 | import { StatusbarProvider } from "./providers/StatusbarProvider"; 13 | import { Events } from "./events/EventHandler"; 14 | 15 | /** 16 | * @namespace Helper 17 | * @description 18 | * Helper provides commands that can be helpfuled in visual studio code extension. 19 | */ 20 | export namespace Helper { 21 | /** 22 | * @class Extension 23 | */ 24 | export class Extension { 25 | private scriptProvider?: ScriptExplorerProvider; 26 | 27 | constructor( 28 | private readonly configService: ConfigService, 29 | private readonly loggingService: LoggingService, 30 | private readonly statusbarProvider: StatusbarProvider, 31 | ) { 32 | this.updateConfiguration(); 33 | } 34 | 35 | setScriptProvider(scriptProvider: ScriptExplorerProvider) { 36 | this.scriptProvider = scriptProvider; 37 | } 38 | 39 | getScriptProvider() { 40 | return this.scriptProvider; 41 | } 42 | 43 | async updateConfiguration() { 44 | const config = 45 | vscode.workspace.getConfiguration("rgssScriptCompiler"); 46 | 47 | if (!config.has("showStatusBar")) { 48 | await config.update("showStatusBar", false); 49 | } 50 | } 51 | 52 | setGamePathCommand() { 53 | return vscode.commands.registerCommand( 54 | "rgss-script-compiler.setGamePath", 55 | async () => { 56 | await setGamePath(this.configService); 57 | this.configService.ON_LOAD_GAME_FOLDER.event( 58 | (gameFolder) => { 59 | Events.emit( 60 | "info", 61 | `Game folder is changed to ${gameFolder}`, 62 | ); 63 | this.statusbarProvider.show(); 64 | }, 65 | ); 66 | }, 67 | ); 68 | } 69 | 70 | saveCommand() { 71 | return vscode.commands.registerCommand( 72 | "rgss-script-compiler.save", 73 | async () => { 74 | await this.configService.detectRGSSVersion(); 75 | await vscode.commands.executeCommand( 76 | "workbench.action.files.save", 77 | ); 78 | await vscode.commands.executeCommand( 79 | "rgss-script-compiler.compile", 80 | ); 81 | }, 82 | ); 83 | } 84 | 85 | testPlayCommand() { 86 | return vscode.commands.registerCommand( 87 | "rgss-script-compiler.testPlay", 88 | () => { 89 | const gamePlayService = new GamePlayService( 90 | this.configService, 91 | this.loggingService, 92 | ); 93 | gamePlayService.run(); 94 | }, 95 | ); 96 | } 97 | 98 | unpackCommand() { 99 | return vscode.commands.registerCommand( 100 | "rgss-script-compiler.unpack", 101 | () => { 102 | if (!this.configService) { 103 | Events.emit("info", "There is no workspace folder."); 104 | return; 105 | } 106 | 107 | const unpacker = new Unpacker( 108 | this.configService, 109 | this.loggingService, 110 | () => { 111 | vscode.commands 112 | .executeCommand( 113 | "rgss-script-compiler.refreshScriptExplorer", 114 | ) 115 | .then(() => { 116 | Events.emit("info", "refreshed"); 117 | }); 118 | }, 119 | ); 120 | unpacker.unpack(); 121 | }, 122 | ); 123 | } 124 | 125 | compileCommand() { 126 | return vscode.commands.registerCommand( 127 | "rgss-script-compiler.compile", 128 | () => { 129 | if (!this.configService) { 130 | Events.emit("info", "There is no workspace folder."); 131 | return; 132 | } 133 | 134 | const bundler = new Packer( 135 | this.configService, 136 | this.loggingService, 137 | ); 138 | bundler.pack(); 139 | }, 140 | ); 141 | } 142 | 143 | /** 144 | * Opens the game folder on Windows or MacOS. 145 | * 146 | */ 147 | openGameFolderCommand() { 148 | return vscode.commands.registerCommand( 149 | "rgss-script-compiler.openGameFolder", 150 | () => { 151 | openGameFolder(this.configService, this.loggingService); 152 | }, 153 | ); 154 | } 155 | 156 | openScriptFileCommand() { 157 | return vscode.commands.registerCommand( 158 | "rgss-script-compiler.openScript", 159 | (scriptFile: vscode.Uri) => { 160 | vscode.window.showTextDocument(scriptFile); 161 | }, 162 | ); 163 | } 164 | 165 | /** 166 | * Gets command elements. 167 | * @returns 168 | */ 169 | getCommands() { 170 | return [ 171 | this.setGamePathCommand(), 172 | this.unpackCommand(), 173 | this.compileCommand(), 174 | this.openGameFolderCommand(), 175 | this.testPlayCommand(), 176 | this.saveCommand(), 177 | this.openScriptFileCommand(), 178 | ]; 179 | } 180 | } 181 | 182 | /** 183 | * @class StatusBarProviderImpl 184 | */ 185 | class StatusBarProviderImpl { 186 | getGameFolderOpenStatusBarItem() { 187 | const statusBarItem = vscode.window.createStatusBarItem( 188 | vscode.StatusBarAlignment.Left, 189 | ); 190 | statusBarItem.text = `$(file-directory) RGSS: Set Game Folder`; 191 | statusBarItem.command = "rgss-script-compiler.setGamePath"; 192 | 193 | return statusBarItem; 194 | } 195 | 196 | getUnpackStatusBarItem() { 197 | const statusBarItem = vscode.window.createStatusBarItem( 198 | vscode.StatusBarAlignment.Left, 199 | ); 200 | statusBarItem.text = `$(sync~spin) RGSS: Import`; 201 | statusBarItem.command = "rgss-script-compiler.unpack"; 202 | 203 | return statusBarItem; 204 | } 205 | 206 | getCompileStatusBarItem() { 207 | const statusBarItem = vscode.window.createStatusBarItem( 208 | vscode.StatusBarAlignment.Left, 209 | ); 210 | statusBarItem.text = `$(sync) RGSS: Compile`; 211 | statusBarItem.command = "rgss-script-compiler.compile"; 212 | 213 | return statusBarItem; 214 | } 215 | 216 | getGameFolderPathStatusBarItem(projectPath: vscode.Uri) { 217 | const statusBarItem = vscode.window.createStatusBarItem( 218 | vscode.StatusBarAlignment.Left, 219 | ); 220 | statusBarItem.text = `$(pulse) Game Path: ${projectPath.fsPath}`; 221 | statusBarItem.backgroundColor = "yellow"; 222 | 223 | return statusBarItem; 224 | } 225 | 226 | getOpenGameFolderButtonItem() { 227 | const statusBarItem = vscode.window.createStatusBarItem( 228 | vscode.StatusBarAlignment.Left, 229 | ); 230 | statusBarItem.text = `$(folder) RGSS: Open Game Folder`; 231 | statusBarItem.command = "rgss-script-compiler.openGameFolder"; 232 | 233 | return statusBarItem; 234 | } 235 | } 236 | 237 | export const StatusBarProvider = new StatusBarProviderImpl(); 238 | 239 | export const getStatusBarProvider = () => { 240 | return StatusBarProvider; 241 | }; 242 | 243 | export const getStatusBarItems = () => { 244 | return [ 245 | Helper.StatusBarProvider.getGameFolderOpenStatusBarItem(), 246 | Helper.StatusBarProvider.getUnpackStatusBarItem(), 247 | Helper.StatusBarProvider.getCompileStatusBarItem(), 248 | Helper.StatusBarProvider.getOpenGameFolderButtonItem(), 249 | ]; 250 | }; 251 | 252 | export const createScriptProviderFunction = ( 253 | helper: Helper.Extension, 254 | configService: ConfigService, 255 | loggingService: LoggingService, 256 | ) => { 257 | if (!helper.getScriptProvider()) { 258 | Events.emit("info", "Importing the scripts...."); 259 | 260 | const scriptViewerPath = Path.resolve( 261 | configService.getMainGameFolder(), 262 | ); 263 | const scriptProvider = new ScriptExplorerProvider( 264 | scriptViewerPath, 265 | loggingService, 266 | configService, 267 | ); 268 | 269 | helper.setScriptProvider(scriptProvider); 270 | 271 | const context = configService.getExtensionContext(); 272 | 273 | const view = vscode.window.createTreeView("rgssScriptViewer", { 274 | treeDataProvider: scriptProvider, 275 | showCollapseAll: true, 276 | canSelectMany: true, 277 | dragAndDropController: scriptProvider, 278 | }); 279 | context.subscriptions.push(view); 280 | } 281 | }; 282 | } 283 | -------------------------------------------------------------------------------- /src/JSerializeObject.ts: -------------------------------------------------------------------------------- 1 | import { RGSS } from "./RGSS"; 2 | 3 | /** 4 | * @class JSerializeObject 5 | * @description This class is responsible for serializing and deserializing the config object. 6 | */ 7 | export class JSerializeObject { 8 | constructor(private readonly data: RGSS.JSerializeData) {} 9 | 10 | toBuffer(): Buffer { 11 | return Buffer.from(JSON.stringify(this.data), "utf8"); 12 | } 13 | 14 | static of(data: Uint8Array): RGSS.JSerializeData { 15 | return JSON.parse(Buffer.from(data).toString("utf8")); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mutex.ts: -------------------------------------------------------------------------------- 1 | export class Mutex { 2 | private mutex = Promise.resolve(); 3 | 4 | lock(): PromiseLike<() => void> { 5 | let begin: (unlock: () => void) => void = (unlock) => {}; 6 | 7 | this.mutex = this.mutex.then(() => { 8 | return new Promise(begin); 9 | }); 10 | 11 | return new Promise((res) => { 12 | begin = res; 13 | }); 14 | } 15 | 16 | async dispatch(fn: (() => T) | (() => PromiseLike)): Promise { 17 | const unlock = await this.lock(); 18 | 19 | try { 20 | return await Promise.resolve(fn()); 21 | } finally { 22 | unlock(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Packer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | compressScriptFiles, 3 | RubyCompressScriptService, 4 | } from "./commands/ExtractScriptFiles"; 5 | import { ConfigService } from "./services/ConfigService"; 6 | import { LoggingService } from "./services/LoggingService"; 7 | import { Unpacker } from "./Unpacker"; 8 | import * as path from "path"; 9 | import { Path } from "./utils/Path"; 10 | 11 | const PACKER_IS_READY = "Packer is ready"; 12 | 13 | export class Packer extends Unpacker { 14 | constructor(configService: ConfigService, loggingService: LoggingService) { 15 | super(configService, loggingService); 16 | } 17 | 18 | /** 19 | * Sets the target file from the main game folder. 20 | * it is assumed that the file extension is one of ruby serialized files(*.rvdata2, *.rvdata, *.rxdata) 21 | */ 22 | initWithTargetFile() { 23 | const root = Path.resolve(this.configService.getMainGameFolder()); 24 | const targetFile = path 25 | .join(root, "Data", ConfigService.TARGET_SCRIPT_FILE_NAME) 26 | .replace(/\\/g, "/"); 27 | 28 | this._targetFile = targetFile; 29 | this._isReady = true; 30 | } 31 | 32 | /** 33 | * This function is reponsible for serializing the ruby script files. 34 | * ? $RGSS_SCRIPTS has three elements such as [section_id, name, compressed_script using zlib] 35 | */ 36 | pack() { 37 | if (!this._isReady) { 38 | this.loggingService.info(PACKER_IS_READY); 39 | throw new Error(PACKER_IS_READY); 40 | } 41 | 42 | this.updateTargetFile(); 43 | const targetFile = this._targetFile; 44 | 45 | try { 46 | // Create ruby script service 47 | const rubyScriptService = new RubyCompressScriptService( 48 | this.configService, 49 | this.loggingService, 50 | { 51 | vscodeWorkspaceFolder: Path.resolve( 52 | this.configService.getVSCodeWorkSpace() 53 | ), 54 | scriptFile: targetFile, 55 | }, 56 | (err: any, stdout: any, stderr: any) => { 57 | if (err) { 58 | this.loggingService.info(err); 59 | } 60 | this.loggingService.info("Job completed."); 61 | } 62 | ); 63 | 64 | compressScriptFiles( 65 | this.loggingService, 66 | rubyScriptService 67 | ); 68 | } catch (e) { 69 | this.loggingService.info((e).message); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/RGSS.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from "vscode"; 3 | 4 | export namespace RGSS { 5 | export type VERSION = "RGSS1" | "RGSS2" | "RGSS3"; 6 | 7 | export type config = { 8 | /** 9 | * Sets or Gets the path of the main game folder. 10 | */ 11 | mainGameFolder?: vscode.Uri; 12 | /** 13 | * Sets or Gets the path of the workspace. 14 | */ 15 | workSpace?: vscode.Uri; 16 | 17 | /** 18 | * Sets or Gets the path of the extension folder. 19 | */ 20 | extensionContext?: vscode.ExtensionContext; 21 | 22 | /** 23 | * Sets or Gets the RGSS version. 24 | */ 25 | rgssVersion?: VERSION; 26 | }; 27 | 28 | export type MapOfPath = "RGSS1" | "RGSS2" | "RGSS3"; 29 | 30 | export type Path = { 31 | [key in MapOfPath]: vscode.Uri; 32 | } & { 33 | RGSS1: vscode.Uri; 34 | RGSS2: vscode.Uri; 35 | RGSS3: vscode.Uri; 36 | }; 37 | 38 | export type JSerializeData = { [key in keyof RGSS.config]: any }; 39 | } 40 | -------------------------------------------------------------------------------- /src/Unpacker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { ConfigService } from "./services/ConfigService"; 5 | import { LoggingService } from "./services/LoggingService"; 6 | import { 7 | extractScriptFiles, 8 | RubyScriptService, 9 | } from "./commands/ExtractScriptFiles"; 10 | import { Path } from "./utils/Path"; 11 | 12 | const message = { 13 | UNPACKER_NOT_READY: "Unpacker is not ready.", 14 | JOB_COMPLETED: "Job completed.", 15 | }; 16 | 17 | namespace RGSS { 18 | export const TARGET_SCRIPT_FILE_NAME = "Scripts.rvdata2"; 19 | export class Unpacker { 20 | protected _targetFile: string; 21 | protected _isReady: boolean; 22 | 23 | constructor( 24 | protected readonly configService: ConfigService, 25 | protected readonly loggingService: LoggingService, 26 | protected readonly successCallback?: () => void, 27 | ) { 28 | this._targetFile = ""; 29 | this._isReady = false; 30 | 31 | this.start(); 32 | } 33 | 34 | start() { 35 | this.initWithTargetFile(); 36 | } 37 | 38 | /** 39 | * Sets the target file from the main game folder. 40 | * it is assumed that the file extension is one of ruby serialized files(*.rvdata2, *.rvdata, *.rxdata) 41 | */ 42 | initWithTargetFile() { 43 | const root = Path.resolve(this.configService.getMainGameFolder()); 44 | const targetFile = path 45 | .join(root, "Data", ConfigService.TARGET_SCRIPT_FILE_NAME) 46 | .replace(/\\/g, "/"); 47 | 48 | if (!fs.existsSync(targetFile)) { 49 | this.loggingService.info(`${targetFile} not found.`); 50 | throw new Error( 51 | `Data/${ConfigService.TARGET_SCRIPT_FILE_NAME} not found.`, 52 | ); 53 | } 54 | 55 | this._targetFile = targetFile; 56 | this._isReady = true; 57 | } 58 | 59 | updateTargetFile() { 60 | this.initWithTargetFile(); 61 | } 62 | 63 | public static isExistFile(configService: ConfigService) { 64 | const root = Path.resolve(configService.getMainGameFolder()); 65 | const targetFile = path 66 | .join(root, "Data", ConfigService.TARGET_SCRIPT_FILE_NAME) 67 | .replace(/\\/g, "/"); 68 | 69 | return !fs.existsSync(targetFile); 70 | } 71 | 72 | /** 73 | * Extract script files to vscode workspace. 74 | */ 75 | unpack() { 76 | if (!this._isReady) { 77 | this.loggingService.info(message.UNPACKER_NOT_READY); 78 | throw new Error(message.UNPACKER_NOT_READY); 79 | } 80 | 81 | this.updateTargetFile(); 82 | const targetFile = this._targetFile; 83 | 84 | try { 85 | // Create ruby script service 86 | const rubyScriptService = new RubyScriptService( 87 | this.configService, 88 | this.loggingService, 89 | { 90 | vscodeWorkspaceFolder: Path.resolve( 91 | this.configService.getVSCodeWorkSpace(), 92 | ), 93 | scriptFile: targetFile, 94 | }, 95 | (err: any, stdout: any, stderr: any) => { 96 | if (err) { 97 | this.loggingService.info(err); 98 | } 99 | this.loggingService.info(message.JOB_COMPLETED); 100 | }, 101 | ); 102 | 103 | extractScriptFiles( 104 | this.loggingService, 105 | rubyScriptService, 106 | this.successCallback, 107 | ); 108 | } catch (e) { 109 | this.loggingService.info((e).message); 110 | } 111 | } 112 | } 113 | } 114 | 115 | export = RGSS; 116 | -------------------------------------------------------------------------------- /src/commands/CheckMigrationNeeded.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { DialogOption } from "../providers/ScriptViewer"; 5 | import { ConfigService } from "../services/ConfigService"; 6 | import { Path } from "../utils/Path"; 7 | 8 | /** 9 | * Check the extension version (0.0.16 -> 0.1.0) 10 | * @param lines 11 | * @returns 12 | */ 13 | export function checkMigrationNeeded(lines: string[]): boolean { 14 | if (lines.length === 0) { 15 | return false; 16 | } 17 | const isLineStartsWithNumber = lines 18 | .filter((line) => line !== "") 19 | .every((line) => { 20 | return line.match(/^[\d]{3}[\.]*[\d]*\-/); 21 | }); 22 | 23 | return !isLineStartsWithNumber; 24 | } 25 | 26 | export function showMigrationNeededErrorMessage(): void { 27 | // vscode.window.showErrorMessage( 28 | // "Your info.txt file is too old. Please update your extension." 29 | // ); 30 | vscode.window.showErrorMessage( 31 | "You need to migrate to use the new version. Please delete the folder named 'Scripts' and import the script again." 32 | ); 33 | } 34 | 35 | function deleteFolder(path: string) { 36 | if (fs.existsSync(path)) { 37 | fs.readdirSync(path).forEach((file) => { 38 | const curPath = `${path}/${file}`; 39 | 40 | if (fs.lstatSync(curPath).isDirectory()) { 41 | // 재귀적으로 하위 폴더 삭제 42 | deleteFolder(curPath); 43 | } else { 44 | // 파일 삭제 45 | fs.unlinkSync(curPath); 46 | } 47 | }); 48 | 49 | // 폴더 삭제 50 | fs.rmdirSync(path); 51 | } 52 | } 53 | 54 | export async function migrationScriptListFile( 55 | scriptFolder: string 56 | ): Promise { 57 | const answer = await vscode.window.showInformationMessage( 58 | "Do you want to migrate?", 59 | DialogOption.YES, 60 | DialogOption.NO 61 | ); 62 | 63 | if (answer === DialogOption.NO) { 64 | return; 65 | } 66 | 67 | if (!fs.existsSync(scriptFolder)) { 68 | return; 69 | } 70 | 71 | deleteFolder(scriptFolder); 72 | 73 | vscode.window.showInformationMessage( 74 | "Scripts folder has deleted. Please import the script again." 75 | ); 76 | 77 | // await vscode.commands.executeCommand("rgss-script-compiler.unpack"); 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/CheckRuby.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | 3 | const COMMAND = "ruby -v"; 4 | 5 | export function isInstalledRuby(): boolean { 6 | let isInstalled = false; 7 | 8 | try { 9 | const stdout = execSync(COMMAND).toString() ?? ""; 10 | 11 | if (stdout.startsWith("ruby")) { 12 | isInstalled = true; 13 | } else { 14 | isInstalled = false; 15 | } 16 | } catch (error: any) { 17 | isInstalled = false; 18 | } 19 | 20 | return isInstalled; 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/CheckWine.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | 3 | const COMMAND = "wine --version"; 4 | 5 | /** 6 | * Checks for Wine availability specific for Linux systems 7 | * @returns boolean 8 | */ 9 | export function isInstalledWine(): boolean { 10 | let isInstalled = false; 11 | 12 | try { 13 | const stdout = execSync(COMMAND).toString() ?? ""; 14 | isInstalled = stdout.startsWith("wine") ? true : false; 15 | } catch (error: any) { 16 | isInstalled = false; 17 | } 18 | 19 | return isInstalled; 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DeleteCommand.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { RGSSScriptSection as ScriptSection } from "../providers/RGSSScriptSection"; 3 | import * as vscode from "vscode"; 4 | import * as fs from "fs"; 5 | import { DependencyProvider } from "../providers/DependencyProvider"; 6 | import { MenuCommand } from "./MenuCommand"; 7 | 8 | enum DialogOption { 9 | YES = "Yes", 10 | NO = "No", 11 | } 12 | 13 | /** 14 | * DeleteCommand allows you to delete a script from the tree safely. 15 | * it is also responsible for updating the list file after deleting the script. 16 | * 17 | * @class DeleteCommand 18 | */ 19 | export class DeleteCommand extends MenuCommand { 20 | constructor(protected dependencyProvider: DependencyProvider) { 21 | super(dependencyProvider); 22 | } 23 | 24 | /** 25 | * This method allows you to delete a script from the tree safely. 26 | * 27 | * @param item 28 | * @returns 29 | */ 30 | public async execute( 31 | item: ScriptSection, 32 | isCopyMode?: boolean, 33 | ): Promise { 34 | if (!isCopyMode) { 35 | const choice = await vscode.window.showInformationMessage( 36 | "Do you want to delete this script?", 37 | DialogOption.YES, 38 | DialogOption.NO, 39 | ); 40 | 41 | if (choice === DialogOption.NO) { 42 | return; 43 | } 44 | } 45 | 46 | this.excludeCurrentSelectFileFromTree(item); 47 | 48 | const targetFilePath = this.getItemFilePath(item); 49 | 50 | try { 51 | await this.createListFile(targetFilePath, item); 52 | await this.view.refreshListFile(); 53 | await vscode.commands.executeCommand( 54 | "rgss-script-compiler.compile", 55 | ); 56 | } catch (error: any) { 57 | vscode.window.showErrorMessage(error.message); 58 | } 59 | } 60 | 61 | /** 62 | * Exclude the current selected file from the tree. 63 | * 64 | * @param item 65 | * @returns 66 | */ 67 | private excludeCurrentSelectFileFromTree(item: ScriptSection) { 68 | if (!this.tree) { 69 | return; 70 | } 71 | 72 | this.tree = this.tree.filter((treeItem) => treeItem.id !== item.id); 73 | } 74 | 75 | private createListFile(targetFilePath: string, item: ScriptSection) { 76 | return new Promise((resolve, reject) => { 77 | this.watcher.executeFileAction("onDidDelete", () => { 78 | if (fs.existsSync(targetFilePath)) { 79 | fs.unlink(targetFilePath, (err) => { 80 | if (item.id) { 81 | this.view.refresh(); 82 | resolve(item.id); 83 | } 84 | 85 | if (err) { 86 | reject(err); 87 | } 88 | }); 89 | } 90 | }); 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/commands/ExtractScriptFiles.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from "../services/ConfigService"; 2 | import * as cp from "child_process"; 3 | import * as path from "path"; 4 | import { LoggingService } from "../services/LoggingService"; 5 | 6 | export type RubyRunnerCommandOptions = { 7 | /** 8 | * Gets or Sets the path of workspace folder 9 | */ 10 | vscodeWorkspaceFolder: string; 11 | 12 | /** 13 | * Gets or Sets the path of the script file in game folder. 14 | * the filename is the same as 'Scripts.rvdata2' in case of RPG Maker VX Ace. 15 | */ 16 | scriptFile: string; 17 | }; 18 | 19 | /** 20 | * This callback function represents for running the Ruby script. 21 | */ 22 | export type RubyProcessCallback = ( 23 | /** 24 | * in case of Javascript or Typescript, the error object returns as any type regardless of the error type. 25 | */ 26 | error: cp.ExecFileException | null, 27 | /** 28 | * This is the standard output of the Ruby script. 29 | */ 30 | stdout: string, 31 | /** 32 | * This is the standard error of the Ruby script. 33 | */ 34 | stderr: string 35 | ) => void; 36 | 37 | /** 38 | * after this callback function is executed, the ruby interpreter process will be terminated on your terminal. 39 | */ 40 | export type RubyProcessOnExitCallback = (code: number, signal: any) => void; 41 | 42 | /** 43 | * This class is responsible for extracting ruby script files after running the Ruby script. 44 | * DI(Dependency Injection) is used for this function. 45 | * so you have to inject the logging service and ruby script service from the outside of this function. 46 | */ 47 | export type ExtractScriptFileFunction = ( 48 | loggingService: LoggingService, 49 | rubyScriptService: RubyScriptService 50 | ) => void; 51 | 52 | /** 53 | * @class RubyScriptService 54 | * @description 55 | * This class is responsible to make the ruby script files after extracting the file that ends with *.rvdata2. 56 | * Data/Scripts.rvdata2 does not be encrypted. it would be decompressed or deserialized using zlib and Marshal.load. 57 | * 58 | * zlib is famous library. so it can use in almost every languages and it is supported it in TypeScript too. 59 | * But Marshal is pretty exclusived in Ruby Languages. so it is not available in Typescript. 60 | * 61 | * How to convert serialized ruby string without Marshal.load of Ruby in Typescript? 62 | * The first one that I think was Marshal module 63 | * 64 | * marshal - https://www.npmjs.com/package/marshal 65 | * 66 | * However it was not work fine. so I used a way to execute ruby directly using node child process module. 67 | * So it's strongly coupled to the ruby language. 68 | * 69 | * This means that you have to install ruby interpreter on your system. 70 | */ 71 | export class RubyScriptService { 72 | protected _process!: cp.ChildProcess | undefined | null; 73 | protected _commandLineOptions: RubyRunnerCommandOptions; 74 | protected _callback: RubyProcessCallback; 75 | protected _args: string[] | undefined; 76 | 77 | get internel() { 78 | return this._process; 79 | } 80 | 81 | /** 82 | * DI(Dependency Injection) is used for this class. 83 | * so you have to inject the configService and the loggingService from the outside of this class. 84 | * 85 | * @param configService 86 | * @param loggingService 87 | * @param header 88 | * @param callback 89 | */ 90 | constructor( 91 | protected readonly configService: ConfigService, 92 | protected readonly loggingService: LoggingService, 93 | header: RubyRunnerCommandOptions, 94 | callback: RubyProcessCallback 95 | ) { 96 | this._commandLineOptions = header; 97 | this._callback = callback; 98 | this._process = null; 99 | this._args = undefined; 100 | 101 | this.makeCommand(); 102 | } 103 | 104 | /** 105 | * This function is responsible for making the command line options. 106 | */ 107 | makeCommand() { 108 | const { vscodeWorkspaceFolder, scriptFile } = this._commandLineOptions; 109 | const extensionPath = 110 | this.configService.getExtensionContext().extensionPath; 111 | 112 | this.loggingService.info( 113 | `The current extension path is ${extensionPath}.` 114 | ); 115 | const rubyFilePath = path.join(extensionPath, "RGSS3", "index.rb"); 116 | 117 | this._args = [ 118 | rubyFilePath, 119 | `--output="${vscodeWorkspaceFolder}"`, 120 | `--input="${scriptFile}"`, 121 | ]; 122 | } 123 | 124 | /** 125 | * Executes the ruby script using the ruby interpreter is installed on your system. 126 | * if the ruby interpreter is not installed, it can't be executed. 127 | */ 128 | run(): void | this { 129 | this._process = cp.execFile( 130 | `ruby`, 131 | this._args, 132 | { 133 | encoding: "utf8", 134 | maxBuffer: 1024 * 1024, 135 | cwd: this.configService.getExtensionContext().extensionPath, 136 | shell: true, 137 | }, 138 | this._callback 139 | ); 140 | if (!this._process) { 141 | return; 142 | } 143 | this._process.stdout!.on("data", (data: any) => { 144 | this.loggingService.info(data); 145 | }); 146 | this._process.stdout!.on("end", (data: any) => { 147 | this.loggingService.info(data); 148 | }); 149 | this._process.stdin!.end(); 150 | return this; 151 | } 152 | 153 | pendingTerminate() { 154 | if (!this._process) { 155 | return; 156 | } 157 | this._process.on("beforeExit", () => this._process!.kill()); 158 | } 159 | 160 | onExit(callback: RubyProcessOnExitCallback) { 161 | if (!this._process) { 162 | return; 163 | } 164 | this._process.on("exit", callback); 165 | } 166 | } 167 | 168 | /** 169 | * @class RubyCompressScriptService 170 | * @description 171 | * This class is responsible for compressing the ruby script files as the file that ends with *.rvdata2 172 | * Data/Scripts.rvdata2 does not be encrypted. it would be compressed or serialized using zlib and Marshal.dump. 173 | * 174 | * zlib is famous library. so it can use in almost every languages and it is supported it in TypeScript too. 175 | * But Marshal is pretty exclusived in Ruby Languages. so it is not available in Typescript. 176 | * 177 | * How to convert serialized ruby string without Marshal.dump of Ruby in Typescript? 178 | * The first one that I think was Marshal module 179 | * 180 | * marshal - https://www.npmjs.com/package/marshal 181 | * 182 | * However it was not work fine. so I used a way to execute ruby directly using node child process module. 183 | * So it's strongly coupled to the ruby language. 184 | * 185 | * This means that you have to install ruby interpreter on your system. 186 | */ 187 | export class RubyCompressScriptService extends RubyScriptService { 188 | /** 189 | * Adds a new argument named '--compress' to inherited command line options. 190 | */ 191 | makeCommand() { 192 | super.makeCommand(); 193 | 194 | this._args?.push("--compress"); 195 | } 196 | } 197 | 198 | /** 199 | * Extracts the game script files after running the Ruby interpreter. 200 | * 201 | * @param loggingService 202 | * @param rubyScriptService 203 | */ 204 | export function extractScriptFiles( 205 | loggingService: LoggingService, 206 | rubyScriptService: RubyScriptService, 207 | successCallback?: () => void 208 | ) { 209 | rubyScriptService.run()!.onExit((code: number, signal: any) => { 210 | loggingService.info(`${code} Script import is complete.`); 211 | if (successCallback) { 212 | successCallback(); 213 | } 214 | }); 215 | rubyScriptService.pendingTerminate(); 216 | } 217 | 218 | /** 219 | * This function is responsible for creating all script files as one script bundle file after running the Ruby interpreter. 220 | * 221 | * @param loggingService 222 | * @param rubyScriptService 223 | */ 224 | export function compressScriptFiles< 225 | T extends RubyCompressScriptService = RubyCompressScriptService 226 | >(loggingService: LoggingService, rubyScriptService: T) { 227 | rubyScriptService.run()!.onExit((code: number, signal: any) => { 228 | loggingService.info(`${code} Script Compile is complete.`); 229 | }); 230 | rubyScriptService.pendingTerminate(); 231 | } 232 | -------------------------------------------------------------------------------- /src/commands/MenuCommand.ts: -------------------------------------------------------------------------------- 1 | import { DependencyProvider } from "../providers/DependencyProvider"; 2 | import { RGSSScriptSection as ScriptSection } from "../providers/RGSSScriptSection"; 3 | import { ScriptTree } from "../providers/ScriptTree"; 4 | import * as path from "path"; 5 | import { Path } from "../utils/Path"; 6 | 7 | export class MenuCommand { 8 | constructor(protected dependencyProvider: DependencyProvider) {} 9 | 10 | protected get view() { 11 | return this.dependencyProvider.view; 12 | } 13 | 14 | protected get tree() { 15 | return this.dependencyProvider.tree!; 16 | } 17 | 18 | protected set tree(tree: ScriptTree) { 19 | this.dependencyProvider.tree = tree; 20 | } 21 | 22 | protected get watcher() { 23 | return this.dependencyProvider.watcher; 24 | } 25 | 26 | protected get workspaceRoot() { 27 | return this.dependencyProvider.workspaceRoot; 28 | } 29 | 30 | protected get scriptDirectory() { 31 | return this.dependencyProvider.scriptDirectory; 32 | } 33 | 34 | /** 35 | * Get the file path of the item. 36 | * 37 | * @param item 38 | * @returns 39 | */ 40 | getItemFilePath(item: ScriptSection) { 41 | return path.posix.join( 42 | this.workspaceRoot, 43 | this.scriptDirectory, 44 | Path.getFileName(item.filePath) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/OpenGameFolder.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ConfigService } from "../services/ConfigService"; 3 | import { LoggingService } from "../services/LoggingService"; 4 | import { exec } from "child_process"; 5 | import * as fs from "fs"; 6 | import * as path from "path"; 7 | import { Path } from "../utils/Path"; 8 | import { promisify } from "util"; 9 | 10 | const execPromise = promisify(exec); 11 | 12 | /** 13 | * Show up the error message on the bottom of the screen. 14 | * 15 | * @param error 16 | */ 17 | function showWarnMessage(loggingService: LoggingService): void { 18 | const platform = process.platform; 19 | 20 | loggingService.info( 21 | `[Open Game Folder] Sorry, but ${platform} is not supported yet.` 22 | ); 23 | } 24 | 25 | /** 26 | * Opens the game folder without vscode extension API. 27 | * 28 | * @param configService 29 | * @param loggingService 30 | */ 31 | export async function openGameFolder( 32 | configService: ConfigService, 33 | loggingService: LoggingService 34 | ): Promise { 35 | try { 36 | const targetFolder = Path.resolve(configService.getMainGameFolder()); 37 | const platform = process.platform; 38 | 39 | switch (platform) { 40 | case "win32": 41 | await execPromise(`explorer ${targetFolder}`); 42 | break; 43 | case "darwin": 44 | await execPromise(`open ${targetFolder}`); 45 | break; 46 | case "linux": // Linux support (xdg-open built-in) 47 | await execPromise(`xdg-open ${targetFolder}`); 48 | break; 49 | } 50 | } catch (e) { 51 | showWarnMessage(loggingService); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/SetGamePath.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ConfigService } from "../services/ConfigService"; 3 | 4 | /** 5 | * This function is responsible for setting the main game folder to config file. 6 | * 7 | * @param configService 8 | * @param loggingService 9 | */ 10 | export async function setGamePath(configService: ConfigService) { 11 | const value = await vscode.window.showOpenDialog({ 12 | canSelectFiles: false, 13 | canSelectFolders: true, 14 | canSelectMany: false, 15 | openLabel: "Set Game Folder", 16 | }); 17 | 18 | if (value) { 19 | await configService.setGameFolder(value[0]); 20 | await configService.saveConfig(); 21 | 22 | // emits on load game folder event. 23 | configService.ON_LOAD_GAME_FOLDER.fire(value[0].fsPath); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/TestGamePlay.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { ConfigService } from "../services/ConfigService"; 3 | import { LoggingService } from "../services/LoggingService"; 4 | import { Path } from "../utils/Path"; 5 | import { promisify } from "util"; 6 | import { exec } from "child_process"; 7 | import * as path from "path"; 8 | import { RubyScriptService } from "./ExtractScriptFiles"; 9 | import * as cp from "child_process"; 10 | import { WorkspaceValue } from "../common/WorkspaceValue"; 11 | import { isInstalledWine } from "./CheckWine"; 12 | import { Validator } from "../utils/Validator"; 13 | import { RGSS } from "../RGSS"; 14 | 15 | const execPromise = promisify(exec); 16 | 17 | export interface GamePlayServiceOptions { 18 | gamePath: string; 19 | cwd: string; 20 | args: string[]; 21 | } 22 | 23 | type GamePlayPrebuiltArgs = { [key in RGSS.MapOfPath]: string[] }; 24 | 25 | /** 26 | * Show up the error message on the bottom of the screen. 27 | * 28 | * @param error 29 | */ 30 | function showWarnMessage(loggingService: LoggingService): void { 31 | const platform = process.platform; 32 | 33 | loggingService.info(`${platform} is not supported yet.`); 34 | } 35 | 36 | export class GamePlayService extends RubyScriptService { 37 | constructor( 38 | protected readonly configService: ConfigService, 39 | protected readonly loggingService: LoggingService 40 | ) { 41 | super( 42 | configService, 43 | loggingService, 44 | { 45 | scriptFile: "", 46 | vscodeWorkspaceFolder: "", 47 | }, 48 | (err: any) => { 49 | if (err) { 50 | this.loggingService.info(err); 51 | } 52 | this.loggingService.info("done"); 53 | } 54 | ); 55 | } 56 | 57 | /** 58 | * This function is responsible for making the command line options. 59 | */ 60 | makeCommand() { 61 | const version = this.configService.getRGSSVersion(); 62 | const platform = process.platform; 63 | const { RGSS1, RGSS2, RGSS3 }: GamePlayPrebuiltArgs = { 64 | RGSS1: ["debug"], 65 | RGSS2: [], 66 | RGSS3: ["console", "test"], 67 | }; 68 | 69 | if (platform === "darwin") { 70 | this._args = []; 71 | return; 72 | } 73 | 74 | switch (version) { 75 | case "RGSS3": 76 | this._args = RGSS3; 77 | break; 78 | case "RGSS2": 79 | this._args = RGSS2; 80 | break; 81 | case "RGSS1": 82 | this._args = RGSS1; 83 | break; 84 | default: 85 | this._args = []; 86 | } 87 | } 88 | 89 | /** 90 | * Executes the ruby script using the ruby interpreter is installed on your system. 91 | * if the ruby interpreter is not installed, it can't be executed. 92 | */ 93 | run(): void | this { 94 | const platform = process.platform; 95 | let target = { 96 | gamePath: "", 97 | cwd: "", 98 | args: [], 99 | }; 100 | 101 | switch (platform) { 102 | case "win32": 103 | target.gamePath = "Game.exe"; 104 | target.args = this._args!; 105 | target.cwd = Path.resolve( 106 | this.configService.getMainGameFolder() 107 | ); 108 | break; 109 | case "darwin": // MKXP-Z supported 110 | target.gamePath = "open"; 111 | target.args = [ 112 | "-b", 113 | ConfigService.getWorkspaceValue( 114 | WorkspaceValue.macOsBundleIdentifier 115 | )!, 116 | Path.join( 117 | ConfigService.getWorkspaceValue( 118 | WorkspaceValue.macOsGamePath 119 | )!, 120 | ".." 121 | ), 122 | ]; 123 | target.cwd = ""; 124 | break; 125 | case "linux": // Linux supported with Wine 126 | this.loggingService.info("Checking for Wine..."); 127 | if (!isInstalledWine()) { 128 | this.loggingService.info( 129 | "Cannot execute test play on Linux without Wine!" 130 | ); 131 | this.loggingService.info( 132 | "Install Wine on your system and try again" 133 | ); 134 | return; 135 | } 136 | this.loggingService.info("Wine is installed!"); 137 | target.gamePath = "wine"; 138 | // EXE for wine plus opt. args, ("." included incase it is not in $PATH) 139 | target.args = ["./Game.exe"].concat(this._args!); 140 | // Resolve POSIX path 141 | target.cwd = Path.resolve( 142 | this.configService.getMainGameFolder() 143 | ); 144 | break; 145 | } 146 | 147 | this._process = cp.execFile( 148 | target.gamePath, 149 | target.args, 150 | { 151 | encoding: "utf8", 152 | maxBuffer: 1024 * 1024, 153 | cwd: target.cwd, 154 | shell: true, 155 | }, 156 | this._callback 157 | ); 158 | if (!this._process) { 159 | return; 160 | } 161 | this._process.stdout!.on("data", (data: any) => { 162 | this.loggingService.info(data); 163 | }); 164 | this._process.stdout!.on("end", (data: any) => { 165 | this.loggingService.info(data); 166 | }); 167 | this._process.stdin!.end(); 168 | return this; 169 | } 170 | } 171 | 172 | /** 173 | * This function is responsible for creating all script files as one script bundle file after running the Ruby interpreter. 174 | * 175 | * @param loggingService 176 | * @param rubyScriptService 177 | */ 178 | export function handleTestPlay( 179 | loggingService: LoggingService, 180 | gamePlayService: T 181 | ): void { 182 | const platform = process.platform; 183 | 184 | if (!Validator.isPlatformOK(platform)) { 185 | showWarnMessage(loggingService); 186 | return; 187 | } 188 | 189 | gamePlayService.run()!.onExit((code: number, signal: any) => { 190 | loggingService.info(`${code} Game.exe file is executed completely.`); 191 | }); 192 | gamePlayService.pendingTerminate(); 193 | } 194 | -------------------------------------------------------------------------------- /src/common/Buttons.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | export enum Buttons { 3 | OK = "OK", 4 | } 5 | -------------------------------------------------------------------------------- /src/common/FileIndexTransformer.ts: -------------------------------------------------------------------------------- 1 | export class FileIndexTransformer { 2 | public static transform(currentIndex: string) { 3 | if (/[\d]+\.[\d]+/g.exec(currentIndex)) { 4 | const [primary, secondary] = currentIndex.split("."); 5 | const isAllNumeric = [primary, secondary].every((e) => 6 | /[\d]+/g.exec(e), 7 | ); 8 | 9 | if (!isAllNumeric) { 10 | const newFileRule = 1000 + Math.floor(Math.random() * 500); 11 | return newFileRule + "-"; 12 | } 13 | 14 | const newFileRule = primary + "." + (parseInt(secondary) + 1); 15 | 16 | return newFileRule + "-"; 17 | } 18 | 19 | const subPrefix = `${currentIndex}.${Math.floor( 20 | Math.random() * 1000, 21 | )}-`; 22 | 23 | return subPrefix; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/Marshal.ts: -------------------------------------------------------------------------------- 1 | import { DumpOptions, LoadOptions, dump, load } from "@hyrious/marshal"; 2 | 3 | export type ScriptTuple = [number, Buffer, Buffer]; 4 | 5 | /** 6 | * @hyrious/marshal의 Ruby Marshal을 래핑하는 클래스입니다. 7 | * CRuby의 Marshal과 호환되며 Ruby Marshal을 사용하는 모든 프로그램에서 사용할 수 있습니다. 8 | * Ruby가 설치되어있지 않은 시스템에서도 Scripts.rvdata2 파일을 읽고 쓸 수 있습니다. 9 | * 10 | * @class Marshal 11 | */ 12 | export class Marshal { 13 | /** 14 | * 직렬화된 스크립트를 읽고 NodeJS에서 읽을 수 있는 형태로 변환합니다. 15 | * 16 | * @param data fs.readFileSync 등으로 읽은 파일 버퍼를 전달하세요. 17 | * @param options 18 | * @returns 19 | */ 20 | static load( 21 | data: string | Uint8Array | ArrayBuffer, 22 | options?: LoadOptions | undefined, 23 | ): ScriptTuple[] { 24 | return load(data, { 25 | ...options, 26 | // 테스트 결과, 이 옵션을 지정하지 않으면 chunk 관련 오류가 납니다. 27 | string: "binary", 28 | }) as ScriptTuple[]; 29 | } 30 | 31 | static dump(data: unknown, options?: DumpOptions | undefined): Uint8Array { 32 | return dump(data, { 33 | ...options, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/common/MessageHelper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | export namespace MessageHelper { 3 | export const ERROR = { 4 | NOT_FOUND_LIST_FILE: 5 | "Cannot find script list file. Please check the game folder. try to reset the game folder.", 6 | }; 7 | 8 | export const INFO = { 9 | OPEN_SCRIPT: "Open Script", 10 | UNTITLED: "Untitled", 11 | NEW_SCRIPT_NAME: "NewScriptName", 12 | FOUND_NODE_BE_DELETED: "Found the node to be deleted", 13 | RELOAD_LIST: `Scripts folder has been deleted. Refreshed the script explorer from info.txt`, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/common/ScriptListFile.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { ConfigService } from "../services/ConfigService"; 3 | import * as vscode from "vscode"; 4 | import * as fs from "fs"; 5 | import * as path from "path"; 6 | import { LoggingService } from "../services/LoggingService"; 7 | import { MessageHelper } from "./MessageHelper"; 8 | import { Path } from "../utils/Path"; 9 | import { RGSSScriptSection } from "../providers/RGSSScriptSection"; 10 | import { generateUUID } from "../utils/uuid"; 11 | 12 | const IGNORE_BLACK_LIST_REGEXP = /(?:Untitled)\_[\d]+/gi; 13 | 14 | export class ScriptListFile { 15 | private _scriptDirectory = "Scripts"; 16 | private static _TMP = ".bak"; 17 | private _lines: string[]; 18 | 19 | constructor( 20 | private readonly configService: ConfigService, 21 | private readonly loggingService: LoggingService, 22 | private readonly workspaceRoot: string, 23 | ) { 24 | this._lines = []; 25 | } 26 | 27 | public get filePath(): string { 28 | const targetFilePath = path.posix.join( 29 | this.workspaceRoot, 30 | this._scriptDirectory, 31 | ConfigService.TARGET_SCRIPT_LIST_FILE_NAME, 32 | ); 33 | 34 | return targetFilePath; 35 | } 36 | 37 | get lines(): string[] { 38 | return this._lines; 39 | } 40 | 41 | get lineCount(): number { 42 | return this._lines.length ?? 0; 43 | } 44 | 45 | /** 46 | * Check whether the script list file exists. 47 | * 48 | * @returns 49 | */ 50 | public isValid(): boolean { 51 | this.loggingService.info(`ScriptListFile: ${this.filePath}`); 52 | if (!fs.existsSync(this.filePath)) { 53 | vscode.window.showErrorMessage( 54 | MessageHelper.ERROR.NOT_FOUND_LIST_FILE, 55 | ); 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * 텍스트 파일을 읽어서 각 라인을 배열로 반환합니다. 64 | * `Untitled_*.rb`로 시작하는 라인은 포함되지 않습니다. 65 | * 66 | * Read lines from the `info.txt` file. 67 | * Note that this method skips the lines that start with `Untitled_.rb`. 68 | * 69 | * @returns 70 | */ 71 | read(): string[] { 72 | const targetFilePath = this.filePath; 73 | 74 | const raw = fs.readFileSync(targetFilePath, "utf8"); 75 | 76 | const lines = raw.split("\n"); 77 | 78 | const scriptList = lines 79 | .map((line) => line.trim()) 80 | .filter((line) => !line.match(IGNORE_BLACK_LIST_REGEXP)); 81 | 82 | this._lines = scriptList; 83 | 84 | return scriptList; 85 | } 86 | 87 | /** 88 | * 텍스트 파일을 읽어서 각 라인을 배열로 반환합니다. 89 | * 90 | * @param skip 91 | * @returns 92 | */ 93 | readAll(skip?: boolean): string[] { 94 | if (skip) { 95 | return this.read(); 96 | } 97 | 98 | const targetFilePath = this.filePath; 99 | const raw = fs.readFileSync(targetFilePath, "utf8"); 100 | const lines = raw.split("\n"); 101 | 102 | return lines; 103 | } 104 | 105 | /** 106 | * 리스트 파일을 라인 별로 읽고 새로운 트리를 생성합니다. 107 | * `Untitled_*.rb`로 시작하는 라인은 포함되지 않습니다. 108 | */ 109 | createScriptSectionFromList(): T[] { 110 | const scriptSections: RGSSScriptSection[] = []; 111 | const COLLAPSED = vscode.TreeItemCollapsibleState.None; 112 | const folderUri = vscode.workspace.workspaceFolders![0].uri; 113 | const fileUri = folderUri.with({ 114 | path: path.posix.join(folderUri.path, this._scriptDirectory), 115 | }); 116 | const lines = this.readAll(); 117 | const { defaultExt } = Path; 118 | 119 | for (const line of lines) { 120 | let isBlankName = false; 121 | let isEmptyContent = false; 122 | 123 | // 빈 라인이거나, 무시할 라인이면 다음 라인으로 넘어갑니다. 124 | if (line.match(IGNORE_BLACK_LIST_REGEXP)) { 125 | isBlankName = true; 126 | } 127 | 128 | let targetScriptSection = ""; 129 | 130 | // 확장자가 .rb로 끝나는 라인을 찾습니다. 131 | if (line.endsWith(defaultExt)) { 132 | targetScriptSection = line.replace(defaultExt, ""); 133 | } 134 | 135 | const targetFilePath = Path.join( 136 | fileUri.path, 137 | targetScriptSection + defaultExt, 138 | ); 139 | 140 | const scriptFilePath = fileUri 141 | .with({ 142 | path: targetFilePath, 143 | }) 144 | .toString(); 145 | 146 | const stat = fs.statSync( 147 | fileUri.with({ 148 | path: targetFilePath, 149 | }).fsPath, 150 | ); 151 | // 빈 파일인지 체크하는 플래그입니다. 152 | if (stat.size === 0) { 153 | isEmptyContent = true; 154 | } 155 | 156 | const scriptSection = new RGSSScriptSection( 157 | isEmptyContent ? "" : targetScriptSection, 158 | COLLAPSED, 159 | scriptFilePath, 160 | ); 161 | 162 | scriptSection.id = generateUUID(); 163 | scriptSection.command = { 164 | command: "vscode.open", 165 | title: MessageHelper.INFO.OPEN_SCRIPT, 166 | arguments: [scriptFilePath], 167 | }; 168 | scriptSections.push(scriptSection); 169 | } 170 | 171 | return scriptSections as T[]; 172 | } 173 | 174 | updateFilename( 175 | scriptFileName: string, 176 | newScriptFileName: string, 177 | ): string[] { 178 | const lines = this.lines.slice(0); 179 | const { defaultExt: ext } = Path; 180 | 181 | let lineIndex = -1; 182 | 183 | for (const line of lines) { 184 | lineIndex++; 185 | 186 | // 빈 라인이거나, 무시할 라인이면 다음 라인으로 넘어갑니다. 187 | if (line.match(IGNORE_BLACK_LIST_REGEXP)) { 188 | continue; 189 | } 190 | 191 | let targetScriptSection = ""; 192 | 193 | // 확장자가 .rb로 끝나는 라인을 찾습니다. 194 | if (line.endsWith(ext)) { 195 | targetScriptSection = line.replace(ext, ""); 196 | } 197 | 198 | // 스크립트 파일명이 같으면 라인 인덱스를 반환합니다. 199 | if (targetScriptSection === scriptFileName) { 200 | break; 201 | } 202 | } 203 | 204 | // 라인 인덱스가 유효하면 해당 라인을 새로운 스크립트 파일명으로 변경합니다. 205 | const temp = lines[lineIndex]; 206 | if (lines[lineIndex]) { 207 | lines[lineIndex] = newScriptFileName; 208 | } 209 | 210 | // 변경된 라인을 로그로 출력합니다. 211 | this.loggingService.info( 212 | `FOUND [${lineIndex}] ${temp} => ${lines[lineIndex]} `, 213 | ); 214 | 215 | return lines; 216 | } 217 | 218 | /** 219 | * 백업 파일을 생성합니다. 220 | */ 221 | async createBackupFile(): Promise { 222 | const { filePath: targetFilePath } = this; 223 | const backupFileName = targetFilePath + ScriptListFile._TMP; 224 | if (fs.existsSync(backupFileName)) { 225 | fs.unlinkSync(backupFileName); 226 | } 227 | 228 | await fs.promises.copyFile(targetFilePath, backupFileName); 229 | } 230 | 231 | async refresh(tree?: T[]): Promise { 232 | if (!tree) { 233 | vscode.window.showErrorMessage("tree parameter is not passed."); 234 | return; 235 | } 236 | 237 | const { filePath: targetFilePath } = this; 238 | const { getFileName, defaultExt } = Path; 239 | 240 | const lines = []; 241 | 242 | await this.createBackupFile(); 243 | 244 | for (const { filePath } of tree) { 245 | // 파일명만 추출 (확장자 포함) 246 | const filename = getFileName(decodeURIComponent(filePath)); 247 | 248 | if (filename === defaultExt) { 249 | continue; 250 | } 251 | 252 | // ! FIXME 2023.03.13 253 | // 파일이 존재하지 않을 때 저장 후 Unpack을 강제로 할 경우, 리스트 파일이 갱신되지 않으면서 모든 파일이 날아가게 된다. 254 | const realFilePath = path.posix.join( 255 | this.workspaceRoot, 256 | this._scriptDirectory, 257 | filename, 258 | ); 259 | 260 | // ! FIXME 2023.03.13 261 | // 모든 파일에 대한 유효성 검증은 필요하지만, continue를 하면 버그 시, 리스트 파일이 비어있게 되므로 continue를 하지 않는다. 262 | if (!fs.existsSync(realFilePath)) { 263 | this.loggingService.info(`${filePath} not found. continue.`); 264 | } 265 | 266 | lines.push(decodeURIComponent(filename)); 267 | } 268 | 269 | const raw = lines.join("\n"); 270 | 271 | await fs.promises.writeFile(targetFilePath, raw, "utf8"); 272 | } 273 | 274 | clear(): void { 275 | this._lines = []; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/common/WorkspaceValue.ts: -------------------------------------------------------------------------------- 1 | export enum WorkspaceValue { 2 | showStatusBar = "rgssScriptCompiler.showStatusBar", 3 | /** 4 | * MKXP-Z 5 | */ 6 | macOsGamePath = "rgssScriptCompiler.macOsGamePath", 7 | 8 | /** 9 | * MKXP-Z 10 | */ 11 | macOsBundleIdentifier = "rgssScriptCompiler.macOsBundleIdentifier", 12 | } 13 | -------------------------------------------------------------------------------- /src/common/encryption/DataManager.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as marshal from "@hyrious/marshal"; 4 | import * as zlib from "zlib"; 5 | 6 | const file = fs.readFileSync( 7 | path.join(process.cwd(), "src", "Scripts.rvdata2"), 8 | ); 9 | 10 | class Marshal { 11 | static load( 12 | data: string | Uint8Array | ArrayBuffer, 13 | options?: marshal.LoadOptions | undefined, 14 | ) { 15 | return marshal.load(data, { 16 | ...options, 17 | string: "binary", 18 | }); 19 | } 20 | 21 | static dump(data: unknown, options?: marshal.DumpOptions | undefined) { 22 | return marshal.dump(data, { 23 | ...options, 24 | }); 25 | } 26 | } 27 | 28 | class Store { 29 | static usedSections: number[] = []; 30 | 31 | static getRandomSection() { 32 | let section: number; 33 | do { 34 | section = Math.floor(Math.random() * 2147483647); 35 | } while (this.usedSections.includes(section)); 36 | this.usedSections.push(section); 37 | return section; 38 | } 39 | static zlibInflate(str: Buffer | string) { 40 | const z = zlib.inflateSync(str); 41 | return z.toString("utf-8"); 42 | } 43 | static zlibDeflate(str: Buffer | string) { 44 | const z = zlib.deflateSync(str); 45 | return z; 46 | } 47 | } 48 | 49 | /** 50 | * Game_System이 포함되어 있는지 확인한다. 51 | */ 52 | function checkGameSystem() { 53 | const scripts = Marshal.load(file) as [number, string][]; 54 | const names = []; 55 | for (const script of scripts) { 56 | const [, name] = script; 57 | const scriptName = Buffer.from(name).toString("utf-8"); 58 | names.push(scriptName); 59 | 60 | console.log(scriptName); 61 | } 62 | 63 | return names.includes("Game_System"); 64 | } 65 | 66 | /** 67 | * 파일을 읽고 Marshal.load를 통해 스크립트 내용을 출력한다. 68 | */ 69 | function printScriptContents() { 70 | const scripts = Marshal.load(file) as [number, Buffer, Buffer][]; 71 | const main = { 72 | name: "", 73 | content: "", 74 | }; 75 | for (const script of scripts) { 76 | const [, name, content] = script; 77 | const scriptContent = Store.zlibInflate(content); 78 | const scriptName = Buffer.from(name).toString("utf-8"); 79 | if (scriptName === "Main") { 80 | main.name = scriptName; 81 | main.content = scriptContent; 82 | } 83 | } 84 | 85 | return main; 86 | } 87 | 88 | /** 89 | * 스크립트를 생성하고 다시 읽는다 90 | */ 91 | function echoScriptContents() { 92 | const outputFile = path.join("src", `output.rvdata2`); 93 | 94 | const dummyScript = "rgss_main { SceneManager.run }"; 95 | const scripts = []; 96 | scripts.push( 97 | [ 98 | Store.getRandomSection(), 99 | Buffer.from("Game_System", "utf-8"), 100 | Store.zlibDeflate(`class Game_System; end`), 101 | ], 102 | [ 103 | Store.getRandomSection(), 104 | Buffer.from("Main", "utf-8"), 105 | Store.zlibDeflate(dummyScript), 106 | ], 107 | ); 108 | const data = Marshal.dump(scripts); 109 | fs.writeFileSync(outputFile, data); 110 | 111 | function read() { 112 | const fileBuffer = fs.readFileSync(outputFile); 113 | const scripts = Marshal.load(fileBuffer) as [number, Buffer, Buffer][]; 114 | const main = { 115 | name: "", 116 | content: "", 117 | }; 118 | for (const script of scripts) { 119 | const [, name, content] = script; 120 | const scriptContent = Store.zlibInflate(content); 121 | const scriptName = Buffer.from(name).toString("utf-8"); 122 | if (scriptName === "Main") { 123 | main.name = scriptName; 124 | main.content = scriptContent; 125 | } 126 | } 127 | return main; 128 | } 129 | const main = read(); 130 | 131 | return main; 132 | } 133 | 134 | /** 135 | * Scripts.rvdata2 파일을 읽고, Marshal.load를 하고, 새로운 스크립트를 추가한다. 136 | * 그리고 다시 Marshal.dump를 통해 output2.rvdata2 파일을 생성한다. 137 | */ 138 | function echoScripts() { 139 | const outputFile = path.join("src", `output2.rvdata2`); 140 | const fileBuffer = fs.readFileSync( 141 | path.join(process.cwd(), "src", "Scripts.rvdata2"), 142 | ); 143 | const scripts = Marshal.load(fileBuffer) as [number, Buffer, Buffer][]; 144 | const result = []; 145 | 146 | for (const script of scripts) { 147 | const [, name, content] = script; 148 | const scriptContent = Store.zlibInflate(content); 149 | const scriptName = Buffer.from(name).toString("utf-8"); 150 | 151 | result.push(script); 152 | 153 | // Main 스크립트 다음에 새로운 내용을 추가한다. 154 | if (scriptName === "Main") { 155 | const newScript = "print 'Hello, World!'"; 156 | const newContent = Store.zlibDeflate(newScript); 157 | const newSection = Store.getRandomSection(); 158 | 159 | result.push([ 160 | newSection, 161 | Buffer.from("NewScript", "utf-8"), 162 | newContent, 163 | ]); 164 | } 165 | } 166 | 167 | const data = Marshal.dump(result); 168 | fs.writeFileSync(outputFile, data); 169 | 170 | // outputFile 파일을 읽는다 171 | const fileBuffer2 = fs.readFileSync(outputFile); 172 | 173 | // Marshal.load를 통해 스크립트를 읽는다. 174 | const scripts2 = Marshal.load(fileBuffer2) as [number, Buffer, Buffer][]; 175 | 176 | for (const script of scripts2) { 177 | const [, name, content] = script; 178 | const scriptContent = Store.zlibInflate(content); 179 | const scriptName = Buffer.from(name).toString("utf-8"); 180 | console.log(scriptName); 181 | console.log(scriptContent); 182 | } 183 | 184 | return true; 185 | } 186 | 187 | console.log(checkGameSystem()); 188 | console.log(printScriptContents()); 189 | console.log(echoScriptContents()); 190 | console.log(echoScripts()); 191 | -------------------------------------------------------------------------------- /src/common/encryption/EncryptionManager.ts: -------------------------------------------------------------------------------- 1 | import * as zlib from "zlib"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import { Events } from "../../events/EventHandler"; 5 | import { Marshal } from "../Marshal"; 6 | import { Path } from "../../utils/Path"; 7 | 8 | export class EncryptionManager { 9 | /** 10 | * Inflate a zlib compressed string. 11 | */ 12 | static zlibInflate(str: Buffer): string { 13 | const z = zlib.inflateSync(str); 14 | 15 | return z.toString("utf-8"); 16 | } 17 | 18 | static zlibDeflate(str: Buffer | string) { 19 | const z = zlib.deflateSync(str); 20 | return z; 21 | } 22 | 23 | static async extractFiles( 24 | vscodeWorkspaceFolder: string, 25 | scriptFile: string, 26 | ) { 27 | Events.emit("info", "Creating index file for the script files."); 28 | 29 | const targetFolder = Path.join(vscodeWorkspaceFolder, "Scripts"); 30 | if (!fs.existsSync(targetFolder)) { 31 | fs.mkdirSync(targetFolder); 32 | } 33 | 34 | const scripts = Marshal.load(scriptFile); 35 | 36 | const scriptNames: string[] = []; 37 | 38 | let scriptIndex = 0; 39 | 40 | for (const script of scripts) { 41 | const [, name, content] = script; 42 | const scriptContent = EncryptionManager.zlibInflate(content); 43 | const scriptName = Buffer.from(name).toString("utf-8"); 44 | 45 | const prefix = scriptIndex.toString().padStart(3, "0"); 46 | 47 | let title = `${prefix}-${scriptName}`; 48 | if (!title || title === "" || title.length === 0) { 49 | title = `${prefix}-Untitled`; 50 | } 51 | 52 | await fs.promises.writeFile( 53 | path.join(targetFolder, `${title}.rb`), 54 | scriptContent, 55 | "utf-8", 56 | ); 57 | 58 | scriptNames.push(title); 59 | scriptIndex++; 60 | } 61 | 62 | fs.writeFileSync( 63 | path.join(targetFolder, "info.txt"), 64 | scriptNames.join("\n"), 65 | "utf-8", 66 | ); 67 | 68 | Events.emit("info", "Script files have been extracted."); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/common/encryption/ScriptStore.ts: -------------------------------------------------------------------------------- 1 | export class ScriptStore { 2 | usedSections: number[] = []; 3 | 4 | getRandomSection(): Readonly { 5 | let section: number; 6 | do { 7 | section = Math.floor(Math.random() * 2147483647); 8 | } while (this.usedSections.includes(section)); 9 | this.usedSections.push(section); 10 | return section; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/encryption/Scripts.rvdata2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/src/common/encryption/Scripts.rvdata2 -------------------------------------------------------------------------------- /src/common/encryption/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./EncryptionManager"; 2 | -------------------------------------------------------------------------------- /src/events/EventHandler.ts: -------------------------------------------------------------------------------- 1 | class EventEmitter { 2 | #events: { [key: string]: Function[] } = {}; 3 | 4 | on(event: string, listener: Function) { 5 | if (!this.#events[event]) { 6 | this.#events[event] = []; 7 | } 8 | this.#events[event].push(listener); 9 | } 10 | 11 | emit(event: string, ...args: any[]) { 12 | if (!this.#events[event]) { 13 | return; 14 | } 15 | this.#events[event].forEach((listener) => listener(...args)); 16 | } 17 | 18 | off(event: string, listener: Function) { 19 | if (!this.#events[event]) { 20 | return; 21 | } 22 | this.#events[event] = this.#events[event].filter((l) => l !== listener); 23 | } 24 | 25 | removeAllListeners(event: string) { 26 | if (!this.#events[event]) { 27 | return; 28 | } 29 | delete this.#events[event]; 30 | } 31 | } 32 | 33 | class EventHandler extends EventEmitter { 34 | constructor() { 35 | super(); 36 | } 37 | } 38 | 39 | export namespace Events { 40 | export const eventHandler = new EventHandler(); 41 | 42 | export function emit(event: string, ...args: T[]) { 43 | eventHandler.emit(event, ...args); 44 | } 45 | 46 | export function on(event: string, listener: Function) { 47 | eventHandler.on(event, listener); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ConfigService } from "./services/ConfigService"; 3 | import { LoggingService } from "./services/LoggingService"; 4 | import * as path from "path"; 5 | import { RGSSScriptSection } from "./providers/RGSSScriptSection"; 6 | import { isInstalledRuby } from "./commands/CheckRuby"; 7 | import { Helper } from "./Helper"; 8 | import { StatusbarProvider } from "./providers/StatusbarProvider"; 9 | import { store } from "./store/GlobalStore"; 10 | import { Events } from "./events/EventHandler"; 11 | 12 | let statusbarProvider: StatusbarProvider; 13 | 14 | /** 15 | * Entry point of the extension. 16 | * 17 | * @param context 18 | */ 19 | export function activate(context: vscode.ExtensionContext) { 20 | // ! Step 1: Logging Service 21 | const loggingService = new LoggingService(); 22 | 23 | Events.on("info", (message: string) => loggingService.info(message)); 24 | Events.emit("info", "RGSS Script Compiler[Extension] has been activated."); 25 | 26 | // ! Step 2: Config Service 27 | const configService = new ConfigService(loggingService); 28 | configService.setExtensionContext(context); 29 | 30 | // ! Step 3: Ruby Check 31 | const isRubyOK = isInstalledRuby(); 32 | Events.emit("info", `Ruby installed: ${isRubyOK}`); 33 | if (!isRubyOK) { 34 | vscode.window.showErrorMessage( 35 | "Can't find Ruby. Please install Ruby and try again.", 36 | ); 37 | } 38 | 39 | store.setIsRubyInstalled(isRubyOK); 40 | 41 | // ! Step 4: Set the workspace folder. 42 | if (!vscode.workspace.workspaceFolders) { 43 | Events.emit("info", "Workspace Folder is not specified."); 44 | throw new Error("Workspace Folder is not specified."); 45 | } 46 | 47 | const workspaces = vscode.workspace.workspaceFolders; 48 | configService.setVSCodeWorkSpace(workspaces[0].uri); 49 | 50 | statusbarProvider = new StatusbarProvider( 51 | context, 52 | loggingService, 53 | configService, 54 | ); 55 | 56 | // ! Step 5 : Create a helper class and set the status bar provider. 57 | const helper = new Helper.Extension( 58 | configService, 59 | loggingService, 60 | statusbarProvider, 61 | ); 62 | 63 | Events.emit("info", "RGSS Script Compiler has executed successfully"); 64 | 65 | statusbarProvider.create(); 66 | 67 | // Load configuration file. 68 | configService 69 | .loadConfig(loggingService) 70 | .then((e) => { 71 | statusbarProvider.initWithGamePath(); 72 | }) 73 | .then((e) => { 74 | Helper.createScriptProviderFunction( 75 | helper, 76 | configService, 77 | loggingService, 78 | ); 79 | Events.emit("info", "configService.loadConfig(loggingService)"); 80 | }) 81 | .catch((e) => { 82 | console.warn(e); 83 | statusbarProvider.hide(); 84 | }); 85 | 86 | loggingService.show(); 87 | 88 | configService.ON_LOAD_GAME_FOLDER.event(() => { 89 | Helper.createScriptProviderFunction( 90 | helper, 91 | configService, 92 | loggingService, 93 | ); 94 | statusbarProvider.show(); 95 | Events.emit("info", "configService.ON_LOAD_GAME_FOLDER.event()"); 96 | }); 97 | 98 | // Sets Subscriptions. 99 | context.subscriptions.push(...helper.getCommands()); 100 | context.subscriptions.push( 101 | ...[ 102 | vscode.commands.registerCommand( 103 | "rgssScriptViewer.refreshEntry", 104 | () => helper.getScriptProvider()?.refresh(), 105 | ), 106 | vscode.commands.registerCommand( 107 | "rgss-script-compiler.deleteFile", 108 | (item: RGSSScriptSection) => { 109 | helper.getScriptProvider()?.deleteTreeItem(item); 110 | }, 111 | ), 112 | vscode.commands.registerCommand( 113 | "rgss-script-compiler.renameFile", 114 | (item: RGSSScriptSection) => { 115 | helper.getScriptProvider()?.changeScriptNameManually(item); 116 | }, 117 | ), 118 | vscode.commands.registerCommand( 119 | "rgss-script-compiler.newFile", 120 | (item: RGSSScriptSection) => { 121 | helper.getScriptProvider()?.addTreeItem(item); 122 | }, 123 | ), 124 | vscode.commands.registerCommand( 125 | "rgss-script-compiler.refreshScriptExplorer", 126 | () => { 127 | loggingService.info("[3]"); 128 | helper.getScriptProvider()?.refreshExplorer(); 129 | }, 130 | ), 131 | vscode.workspace.onDidDeleteFiles((e) => { 132 | e.files.forEach((e) => { 133 | if ( 134 | path.posix.join(e.path).includes("rgss-compiler.json") 135 | ) { 136 | Events.emit("info", "rgss-compiler.json is deleted."); 137 | 138 | statusbarProvider.hide(); 139 | } 140 | }); 141 | }), 142 | ], 143 | ); 144 | context.subscriptions.push( 145 | ...[ 146 | vscode.commands.registerCommand( 147 | "rgss-script-compiler.importAuto", 148 | () => { 149 | vscode.commands 150 | .executeCommand("rgss-script-compiler.setGamePath") 151 | .then((_) => { 152 | return vscode.commands.executeCommand( 153 | "rgss-script-compiler.unpack", 154 | () => {}, 155 | ); 156 | }); 157 | }, 158 | ), 159 | ], 160 | ); 161 | } 162 | 163 | /** 164 | * When deactivating the extension, this function calls. 165 | */ 166 | export function deactivate() { 167 | statusbarProvider.hide(); 168 | } 169 | -------------------------------------------------------------------------------- /src/providers/DependencyProvider.ts: -------------------------------------------------------------------------------- 1 | import { ScriptTree } from "./ScriptTree"; 2 | import { RGSSScriptSection as ScriptSection } from "./RGSSScriptSection"; 3 | import { TreeFileWatcher } from "./TreeFileWatcher"; 4 | import { ScriptExplorerProvider } from "./ScriptViewer"; 5 | 6 | export class DependencyProvider { 7 | constructor( 8 | public _tree: ScriptTree, 9 | public workspaceRoot: string, 10 | public scriptDirectory: string, 11 | public watcher: TreeFileWatcher, 12 | // public scriptService: ScriptService, 13 | public view: ScriptExplorerProvider 14 | ) {} 15 | 16 | public get tree(): ScriptTree | undefined { 17 | return this.view.getTree(); 18 | } 19 | 20 | public set tree(tree: ScriptTree | undefined) { 21 | this.view.setTree(tree!); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/providers/RGSSScriptSection.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * @class RGSSScriptSection 5 | */ 6 | export class RGSSScriptSection extends vscode.TreeItem { 7 | constructor( 8 | public readonly label: string, 9 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 10 | public readonly filePath: string, 11 | ) { 12 | super(label, collapsibleState); 13 | this.tooltip = `${this.label}`; 14 | this.description = `${this.label}`; 15 | 16 | if (label.length > 0) { 17 | this.iconPath = new vscode.ThemeIcon( 18 | label.startsWith("▼") ? "notifications-collapse" : "file", 19 | ); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/providers/ScriptTree.ts: -------------------------------------------------------------------------------- 1 | import { generateUUID } from "../utils/uuid"; 2 | import { RGSSScriptSection } from "./RGSSScriptSection"; 3 | 4 | type FilterPredicate = (value: T, index: number, array: T[]) => boolean; 5 | 6 | export class ScriptTree { 7 | data: T[]; 8 | uuid: string; 9 | 10 | constructor(array: T[]) { 11 | this.data = array; 12 | this.uuid = generateUUID(); 13 | } 14 | 15 | public filter(callback: FilterPredicate): ScriptTree { 16 | return new ScriptTree(this.data.filter(callback)); 17 | } 18 | 19 | public splice( 20 | start: number, 21 | deleteCount: number, 22 | ...items: T[] 23 | ): ScriptTree { 24 | return new ScriptTree(this.data.splice(start, deleteCount, ...items)); 25 | } 26 | 27 | public findIndex(callback: FilterPredicate): number { 28 | return this.data.findIndex(callback); 29 | } 30 | 31 | public find(callback: FilterPredicate): T | undefined { 32 | return this.data.find(callback); 33 | } 34 | 35 | public replaceTree(id: string | undefined, newItem: T): ScriptTree { 36 | const index = this.findIndex((item) => item.id === id); 37 | return this.splice(index, 1, newItem); 38 | } 39 | 40 | get length(): number { 41 | return this.data.length; 42 | } 43 | 44 | /** 45 | * for ... of 46 | * @returns 47 | */ 48 | public [Symbol.iterator](): Iterator { 49 | return this.data[Symbol.iterator](); 50 | } 51 | 52 | public getChildren(): T[] { 53 | return this.data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/providers/ScriptViewer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from "vscode"; 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | import { RGSSScriptSection as ScriptSection } from "./RGSSScriptSection"; 6 | import { ConfigService } from "../services/ConfigService"; 7 | import { LoggingService } from "../services/LoggingService"; 8 | import { Path } from "../utils/Path"; 9 | import { generateUUID } from "../utils/uuid"; 10 | import { Validator } from "../utils/Validator"; 11 | import { TreeFileWatcher } from "./TreeFileWatcher"; 12 | import { ScriptTree } from "./ScriptTree"; 13 | import { MessageHelper } from "../common/MessageHelper"; 14 | import { DeleteCommand } from "../commands/DeleteCommand"; 15 | import { DependencyProvider } from "./DependencyProvider"; 16 | import { 17 | checkMigrationNeeded, 18 | showMigrationNeededErrorMessage, 19 | } from "../commands/CheckMigrationNeeded"; 20 | import { FileIndexTransformer } from "../common/FileIndexTransformer"; 21 | 22 | export enum LoggingMarker { 23 | CREATED = "created", 24 | CHANGED = "changed", 25 | DELETED = "deleted", 26 | RENAME = "rename", 27 | } 28 | 29 | export enum DialogOption { 30 | YES = "Yes", 31 | NO = "No", 32 | } 33 | const IGNORE_BLACK_LIST_REGEXP = /^[\d]{3}\-(?:Untitled)\_[\d]+/gi; 34 | const DND_TREE_VIEW_ID = "application/vnd.code.tree.rgssScriptViewer"; 35 | 36 | export class ScriptExplorerProvider 37 | implements 38 | vscode.TreeDataProvider, 39 | vscode.TreeDragAndDropController, 40 | vscode.Disposable 41 | { 42 | dropMimeTypes = [DND_TREE_VIEW_ID]; 43 | dragMimeTypes = ["text/uri-list"]; 44 | 45 | private _scriptDirectory = "Scripts"; 46 | private _watcher?: TreeFileWatcher; 47 | private _scriptFolderRootWatcher?: TreeFileWatcher; 48 | private _tree?: ScriptTree; 49 | 50 | constructor( 51 | private workspaceRoot: string, 52 | private readonly loggingService: LoggingService, 53 | private readonly configService: ConfigService, 54 | ) { 55 | this.initWithFileWatcher(); 56 | this.initWithScriptFolderWatcher(); 57 | this._tree = new ScriptTree([]); 58 | } 59 | 60 | private _onDidChangeTreeData: vscode.EventEmitter< 61 | ScriptSection | undefined | null | void 62 | > = new vscode.EventEmitter(); 63 | readonly onDidChangeTreeData: vscode.Event< 64 | ScriptSection | undefined | null | void 65 | > = this._onDidChangeTreeData.event; 66 | 67 | /** 68 | * Create the file watcher for the script directory. 69 | */ 70 | initWithFileWatcher() { 71 | this._watcher = new TreeFileWatcher(this.loggingService); 72 | 73 | this._watcher.create(); 74 | 75 | this._watcher.onDidRenameFiles.event(({ oldUrl, newUrl }) => { 76 | this.onDidRenameFiles(oldUrl, newUrl); 77 | }); 78 | this._watcher.onDidCreate.event((file) => { 79 | this.onDidCreate(file); 80 | }); 81 | this._watcher.onDidChange.event((file) => { 82 | this.onDidChange(file); 83 | }); 84 | this._watcher.onDidDelete.event((file) => { 85 | this.onDidDelete(file); 86 | }); 87 | } 88 | 89 | initWithScriptFolderWatcher() { 90 | this._scriptFolderRootWatcher = new TreeFileWatcher( 91 | this.loggingService, 92 | "**/Scripts", 93 | ); 94 | this._scriptFolderRootWatcher.create(); 95 | 96 | this._scriptFolderRootWatcher.onDidDelete.event((uri) => { 97 | this.loggingService.info(MessageHelper.INFO.RELOAD_LIST); 98 | 99 | this.refreshExplorer(); 100 | }); 101 | } 102 | 103 | /** 104 | * Release the file watcher and other resources. 105 | */ 106 | dispose() { 107 | this._watcher?.dispose(); 108 | this._scriptFolderRootWatcher?.dispose(); 109 | } 110 | 111 | private onDidRenameFiles(oldUrl: vscode.Uri, newUrl: vscode.Uri) { 112 | const oldScriptSection = this._tree?.find((item) => { 113 | return ( 114 | Path.getFileName(item.label) + ".rb" === 115 | Path.getFileName(oldUrl.fsPath) 116 | ); 117 | }); 118 | 119 | if (!oldScriptSection) { 120 | this.loggingService.info(`[INFO] oldScriptSection not found!`); 121 | return; 122 | } 123 | 124 | if (oldScriptSection) { 125 | this.renameTreeItem(oldScriptSection, newUrl); 126 | } 127 | } 128 | 129 | /** 130 | * 드롭 시 호출되는 이벤트 131 | * 132 | * @param target 옮길 위치 133 | * @param sources 드래그한 트리 노드 134 | * @param token 135 | * @returns 136 | */ 137 | public async handleDrop( 138 | target: ScriptSection | undefined, 139 | sources: vscode.DataTransfer, 140 | token: vscode.CancellationToken, 141 | ): Promise { 142 | const transferItem = sources.get(DND_TREE_VIEW_ID); 143 | if (!transferItem) { 144 | return; 145 | } 146 | 147 | const source = transferItem.value as ScriptSection[]; 148 | 149 | if (source.length === 0) { 150 | return; 151 | } 152 | 153 | const oldItem = source[0]; 154 | const newItem = target; 155 | 156 | if (!newItem) { 157 | return; 158 | } 159 | 160 | if (oldItem.id === newItem.id) { 161 | return; 162 | } 163 | 164 | this.moveScriptSection(oldItem, newItem); 165 | } 166 | 167 | /** 168 | * 169 | * @param source 170 | * @param dataTransfer 171 | * @param token 172 | */ 173 | 174 | public handleDrag( 175 | source: readonly ScriptSection[], 176 | dataTransfer: vscode.DataTransfer, 177 | token: vscode.CancellationToken, 178 | ): void | Thenable { 179 | dataTransfer.set(DND_TREE_VIEW_ID, new vscode.DataTransferItem(source)); 180 | } 181 | 182 | /** 183 | * 드래그를 통해 이동한 스크립트를 탐색기에 반영합니다. 184 | * 185 | * @param oldItem 186 | * @param newItem 187 | */ 188 | private moveScriptSection(oldItem: ScriptSection, newItem: ScriptSection) { 189 | const oldIndex = this._tree?.findIndex( 190 | (item) => item.id === oldItem.id, 191 | ); 192 | const newIndex = this._tree?.findIndex( 193 | (item) => item.id === newItem.id, 194 | ); 195 | 196 | if (oldIndex !== undefined && newIndex !== undefined) { 197 | this._tree?.splice(oldIndex, 1); 198 | this._tree?.splice(newIndex, 0, oldItem); 199 | } 200 | 201 | this.refresh(); 202 | this.refreshListFile(); 203 | } 204 | 205 | private renameTreeItem(oldItem: ScriptSection, newUrl: vscode.Uri) { 206 | const label = Path.getFileName(newUrl.fsPath, Path.defaultExt); 207 | 208 | const targetFilePath = path.posix.join( 209 | this.workspaceRoot, 210 | this._scriptDirectory, 211 | label + Path.defaultExt, 212 | ); 213 | 214 | // Create a new tree item 215 | const newScriptSection = { 216 | ...oldItem, 217 | }; 218 | 219 | newScriptSection.id = generateUUID(); 220 | newScriptSection.label = label; 221 | newScriptSection.filePath = targetFilePath; 222 | newScriptSection.command = { 223 | command: "vscode.open", 224 | title: MessageHelper.INFO.OPEN_SCRIPT, 225 | arguments: [vscode.Uri.file(targetFilePath).path], 226 | }; 227 | 228 | this.replaceLineByFilename(oldItem.label, label); 229 | 230 | this.refresh(); 231 | this.refreshListFile(); 232 | } 233 | 234 | private onDidCreate(url: vscode.Uri) { 235 | this.loggingService.info( 236 | `[file ${LoggingMarker.CREATED}] ${JSON.stringify(url)}`, 237 | ); 238 | 239 | const COLLAPSED = vscode.TreeItemCollapsibleState.None; 240 | 241 | const name = Path.getFileName(url.fsPath, Path.defaultExt); 242 | const section = new ScriptSection(name, COLLAPSED, url.fsPath); 243 | 244 | section.id = generateUUID(); 245 | section.command = { 246 | command: "vscode.open", 247 | title: MessageHelper.INFO.OPEN_SCRIPT, 248 | arguments: [vscode.Uri.file(url.fsPath)], 249 | }; 250 | } 251 | 252 | private onDidChange(url: vscode.Uri) { 253 | this.loggingService.info( 254 | `[file ${LoggingMarker.CHANGED}] ${JSON.stringify(url)}`, 255 | ); 256 | } 257 | 258 | /** 259 | * Refresh the script explorer after applying the changes. 260 | */ 261 | private onDidDelete(url: vscode.Uri) { 262 | this.loggingService.info( 263 | `[file ${LoggingMarker.DELETED}] ${JSON.stringify(url)}`, 264 | ); 265 | 266 | const scriptSection = this._tree?.find( 267 | (item) => 268 | Path.getFileName(item.filePath) === 269 | Path.getFileName(url.fsPath), 270 | ); 271 | 272 | if (scriptSection) { 273 | this.loggingService.info(MessageHelper.INFO.FOUND_NODE_BE_DELETED); 274 | this.deleteTreeItem(scriptSection); 275 | } 276 | } 277 | 278 | /** 279 | * 스크립트 탐색기를 갱신합니다. 280 | */ 281 | refresh(): void { 282 | this._onDidChangeTreeData.fire(); 283 | } 284 | 285 | getTreeItem( 286 | element: ScriptSection, 287 | ): vscode.TreeItem | Thenable { 288 | return element; 289 | } 290 | 291 | getChildren( 292 | element?: ScriptSection | undefined, 293 | ): vscode.ProviderResult { 294 | if (!this.workspaceRoot) { 295 | return []; 296 | } 297 | 298 | if (this._tree?.length === 0) { 299 | return this.parseScriptSectionFromList(); 300 | } 301 | 302 | return this._tree?.getChildren(); 303 | } 304 | 305 | /** 306 | * Delete a script section from the tree data. 307 | * 308 | * @param item 309 | */ 310 | async deleteTreeItem( 311 | item: ScriptSection, 312 | isCopyMode?: boolean, 313 | ): Promise { 314 | if (!this._tree || !this._watcher) { 315 | return; 316 | } 317 | const dependencyProvider = new DependencyProvider( 318 | this._tree, 319 | this.workspaceRoot, 320 | this._scriptDirectory, 321 | this._watcher, 322 | this, 323 | ); 324 | 325 | const deleteCommand = new DeleteCommand(dependencyProvider); 326 | 327 | await deleteCommand.execute(item, isCopyMode); 328 | 329 | if (!isCopyMode) { 330 | this.hideActiveScript(); 331 | } 332 | } 333 | 334 | async changeScriptNameManually(item: ScriptSection) { 335 | const isCopyMode = true; 336 | 337 | if (await this.addTreeItem(item, isCopyMode)) { 338 | await this.deleteTreeItem(item, isCopyMode); 339 | } 340 | } 341 | 342 | setTree(tree: ScriptTree) { 343 | this._tree = tree; 344 | } 345 | 346 | getTree(): ScriptTree | undefined { 347 | return this._tree; 348 | } 349 | 350 | /** 351 | * Add a new script section to the tree data. 352 | * 353 | * @param item {ScriptSection} The new script section. 354 | */ 355 | async addTreeItem( 356 | item: ScriptSection, 357 | isCopyMode?: boolean, 358 | ): Promise { 359 | // Enter the script name 360 | const result = await vscode.window.showInputBox({ 361 | prompt: isCopyMode 362 | ? "Please enter the script name you want to change" 363 | : "Please a new script name.", 364 | value: isCopyMode 365 | ? MessageHelper.INFO.NEW_SCRIPT_NAME 366 | : MessageHelper.INFO.UNTITLED, 367 | validateInput: (value: string) => { 368 | if (!Validator.isStringOrNotEmpty(value)) { 369 | return Validator.PLASE_INPUT_SCR_NAME; 370 | } 371 | 372 | if (!Validator.isValidScriptName(value)) { 373 | return Validator.INVALID_SCRIPT_NAME; 374 | } 375 | 376 | return Validator.VALID; 377 | }, 378 | }); 379 | 380 | if (result) { 381 | const prefix = Path.getFileName(item.filePath).split("-"); 382 | const subPrefix = FileIndexTransformer.transform(prefix?.[0] ?? ""); 383 | 384 | // Create a new empty script file 385 | const targetFilePath = path.posix.join( 386 | this.workspaceRoot, 387 | this._scriptDirectory, 388 | subPrefix + result + Path.defaultExt, // 098.1-Test.rb 389 | ); 390 | 391 | this.loggingService.info("subPrefix: " + subPrefix); 392 | 393 | this._watcher?.executeFileAction("onDidCreate", () => {}); 394 | 395 | let readContents = undefined; 396 | if (isCopyMode && fs.existsSync(item.filePath)) { 397 | readContents = fs.readFileSync(item.filePath, "utf8"); 398 | 399 | this.loggingService.info( 400 | `[INFO] readContents: ${readContents}`, 401 | ); 402 | } 403 | 404 | if (!fs.existsSync(targetFilePath)) { 405 | fs.writeFileSync( 406 | targetFilePath, 407 | readContents ? readContents : "", 408 | "utf8", 409 | ); 410 | } 411 | 412 | const targetIndex = this._tree?.findIndex( 413 | (treeItem) => treeItem.id === item.id, 414 | ); 415 | 416 | const copiedItem = { 417 | ...item, 418 | }; 419 | 420 | copiedItem.id = generateUUID(); 421 | copiedItem.label = result; 422 | copiedItem.filePath = targetFilePath; 423 | copiedItem.command = { 424 | command: "vscode.open", 425 | title: MessageHelper.INFO.OPEN_SCRIPT, 426 | arguments: [vscode.Uri.file(targetFilePath).path], 427 | }; 428 | 429 | this._tree?.splice(targetIndex! + 1, 0, copiedItem); 430 | 431 | this.refresh(); 432 | this.refreshListFile(); 433 | 434 | this.showScript(vscode.Uri.file(targetFilePath)); 435 | 436 | return true; 437 | } 438 | 439 | return false; 440 | } 441 | 442 | /** 443 | * Show the script file in the editor. 444 | * @param file 445 | */ 446 | showScript(file: vscode.Uri) { 447 | vscode.commands.executeCommand("workbench.action.closeActiveEditor"); 448 | vscode.window.showTextDocument(file); 449 | } 450 | 451 | /** 452 | * Hide the active script file in the editor. 453 | */ 454 | hideActiveScript() { 455 | vscode.commands.executeCommand("workbench.action.closeActiveEditor"); 456 | } 457 | 458 | replaceLineByFilename(label: string, newLabel: string) { 459 | // read the list file 460 | const targetFilePath = path.posix.join( 461 | this.workspaceRoot, 462 | this._scriptDirectory, 463 | ConfigService.TARGET_SCRIPT_LIST_FILE_NAME, 464 | ); 465 | 466 | if (!fs.existsSync(targetFilePath)) { 467 | vscode.window.showErrorMessage( 468 | MessageHelper.ERROR.NOT_FOUND_LIST_FILE, 469 | ); 470 | return []; 471 | } 472 | 473 | const raw = fs.readFileSync(targetFilePath, "utf8"); 474 | const lines = raw.split("\n"); 475 | 476 | let lineIndex = -1; 477 | 478 | for (const line of lines) { 479 | lineIndex++; 480 | if (line.match(IGNORE_BLACK_LIST_REGEXP)) { 481 | continue; 482 | } 483 | 484 | let targetScriptSection = ""; 485 | 486 | if (line.endsWith(Path.defaultExt)) { 487 | targetScriptSection = line.replace(Path.defaultExt, ""); 488 | } 489 | 490 | if (targetScriptSection === label) { 491 | break; 492 | } 493 | } 494 | 495 | const temp = lines[lineIndex]; 496 | if (lines[lineIndex]) { 497 | lines[lineIndex] = newLabel; 498 | } 499 | 500 | this.loggingService.info( 501 | `FOUND [${lineIndex}] ${temp} => ${lines[lineIndex]} `, 502 | ); 503 | 504 | return lines; 505 | } 506 | 507 | /** 508 | * Creates the text file called info.txt, which contains the script title. 509 | * This file is used to display the script title in the script explorer. 510 | * it is used to packing or unpacking the script in the ruby interpreter. 511 | * 512 | * @deprecated 513 | */ 514 | async refreshListFile() { 515 | const targetFilePath = path.posix.join( 516 | this.workspaceRoot, 517 | this._scriptDirectory, 518 | ConfigService.TARGET_SCRIPT_LIST_FILE_NAME, 519 | ); 520 | 521 | const lines = []; 522 | 523 | // 백업 파일을 생성한다. 524 | const backupFileName = targetFilePath + ".bak"; 525 | if (fs.existsSync(backupFileName)) { 526 | fs.unlinkSync(backupFileName); 527 | } 528 | 529 | await fs.promises.copyFile(targetFilePath, backupFileName); 530 | 531 | for (const { filePath } of this._tree!) { 532 | // 확장자를 포함하여 파일명을 추출한다. 533 | const filename = Path.getFileName(decodeURIComponent(filePath)); 534 | 535 | // ! FIXME 2023.03.13 536 | // 파일이 존재하지 않을 때 저장 후 Unpack을 강제로 할 경우, 리스트 파일이 갱신되지 않으면서 모든 파일이 날아가게 된다. 537 | const realFilePath = path.posix.join( 538 | this.workspaceRoot, 539 | this._scriptDirectory, 540 | filename, 541 | ); 542 | 543 | // ! FIXME 2023.03.13 544 | // 모든 파일에 대한 유효성 검증은 필요하지만, continue를 하면 버그 시, 리스트 파일이 비어있게 되므로 continue를 하지 않는다. 545 | if (!fs.existsSync(realFilePath)) { 546 | this.loggingService.info(`${filePath} not found. continue.`); 547 | } 548 | 549 | lines.push(decodeURIComponent(filename)); 550 | } 551 | 552 | const raw = lines.join("\n"); 553 | 554 | await fs.promises.writeFile(targetFilePath, raw, "utf8"); 555 | } 556 | 557 | refreshExplorer() { 558 | this._tree = new ScriptTree([]); 559 | 560 | this.refresh(); 561 | } 562 | 563 | /** 564 | * 루비로 작성된 스크립트 추출기를 통해 스크립트 목록 파일을 생성합니다. 565 | * 이렇게 생성된 info.txt 파일로부터 스크립트 목록을 파싱하여 트리 데이터를 생성합니다. 566 | */ 567 | private parseScriptSectionFromList(): ScriptSection[] { 568 | const targetFilePath = path.posix.join( 569 | this.workspaceRoot, 570 | this._scriptDirectory, 571 | ConfigService.TARGET_SCRIPT_LIST_FILE_NAME, 572 | ); 573 | 574 | if (!fs.existsSync(targetFilePath)) { 575 | vscode.window.showErrorMessage( 576 | MessageHelper.ERROR.NOT_FOUND_LIST_FILE, 577 | ); 578 | return []; 579 | } 580 | 581 | const raw = fs.readFileSync(targetFilePath, "utf8"); 582 | 583 | const lines = raw.split("\n"); 584 | const scriptSections: ScriptSection[] = []; 585 | 586 | const COLLAPSED = vscode.TreeItemCollapsibleState.None; 587 | 588 | const folderUri = vscode.workspace.workspaceFolders![0].uri; 589 | const fileUri = folderUri.with({ 590 | path: path.posix.join(folderUri.path, this._scriptDirectory), 591 | }); 592 | 593 | if (checkMigrationNeeded(lines)) { 594 | showMigrationNeededErrorMessage(); 595 | return []; 596 | } 597 | 598 | for (const line of lines) { 599 | let isBlankName = false; 600 | let isEmptyContent = false; 601 | 602 | if (line.match(IGNORE_BLACK_LIST_REGEXP)) { 603 | isBlankName = true; 604 | } 605 | 606 | let targetScriptSection = ""; 607 | 608 | if (line.trim() === "") { 609 | continue; 610 | } 611 | 612 | if (line.endsWith(Path.defaultExt)) { 613 | targetScriptSection = line.replace(Path.defaultExt, ""); 614 | } 615 | 616 | const scriptFilePath = fileUri 617 | .with({ 618 | path: path.posix.join( 619 | fileUri.path, 620 | targetScriptSection + Path.defaultExt, 621 | ), 622 | }) 623 | .toString(); 624 | 625 | const stat = fs.statSync( 626 | fileUri.with({ 627 | path: Path.join( 628 | fileUri.path, 629 | targetScriptSection + Path.defaultExt, 630 | ), 631 | }).fsPath, 632 | ); 633 | if (stat.size === 0) { 634 | isEmptyContent = true; 635 | } 636 | 637 | const scriptSection = new ScriptSection( 638 | targetScriptSection.match(/^[\d]{3}\-(?:Untitled)/g) 639 | ? "" 640 | : targetScriptSection.replace(/^[\d]{3}\-/, ""), 641 | COLLAPSED, 642 | scriptFilePath, 643 | ); 644 | 645 | // Create a tree item for the script explorer. 646 | scriptSection.id = generateUUID(); 647 | scriptSection.command = { 648 | command: "vscode.open", 649 | title: MessageHelper.INFO.OPEN_SCRIPT, 650 | arguments: [scriptFilePath], 651 | }; 652 | scriptSections.push(scriptSection); 653 | } 654 | 655 | this._tree = new ScriptTree(scriptSections); 656 | 657 | return scriptSections; 658 | } 659 | } 660 | -------------------------------------------------------------------------------- /src/providers/StatusbarProvider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | import * as vscode from "vscode"; 3 | import { ConfigService } from "../services/ConfigService"; 4 | import { Helper } from "../Helper"; 5 | import { LoggingService } from "../services/LoggingService"; 6 | import { WorkspaceValue } from "../common/WorkspaceValue"; 7 | 8 | interface IStatusbarProvider { 9 | show(): void; 10 | hide(): void; 11 | } 12 | 13 | export class StatusbarProvider 14 | implements IStatusbarProvider, vscode.Disposable 15 | { 16 | private _items?: vscode.StatusBarItem[]; 17 | private _gameFolderPath?: vscode.StatusBarItem | undefined; 18 | 19 | constructor( 20 | private readonly context: vscode.ExtensionContext, 21 | private readonly loggingService: LoggingService, 22 | private readonly configService: ConfigService 23 | ) {} 24 | 25 | create(): void { 26 | this.initializeWithItems(); 27 | this.onDidChangeConfiguration(); 28 | } 29 | 30 | initializeWithItems(): void { 31 | const { context } = this; 32 | 33 | this._items = ConfigService.getWorkspaceValue( 34 | WorkspaceValue.showStatusBar 35 | ) 36 | ? Helper.getStatusBarItems() 37 | : []; 38 | 39 | context.subscriptions.push(...this._items); 40 | } 41 | 42 | initWithGamePath(): void { 43 | const { context } = this; 44 | const provider = Helper.StatusBarProvider; 45 | 46 | const config = this.configService.getConfig(); 47 | 48 | this._gameFolderPath = provider.getGameFolderPathStatusBarItem( 49 | config.mainGameFolder! 50 | ); 51 | 52 | context.subscriptions.push(this._gameFolderPath); 53 | } 54 | 55 | onDidChangeConfiguration(context?: vscode.ExtensionContext): void { 56 | if (!context) { 57 | context = this.context; 58 | } 59 | 60 | const sectionKey = WorkspaceValue.showStatusBar; 61 | 62 | context.subscriptions.push( 63 | vscode.workspace.onDidChangeConfiguration((e) => { 64 | const provider = Helper.getStatusBarProvider(); 65 | 66 | if (e.affectsConfiguration(sectionKey)) { 67 | const config = vscode.workspace 68 | .getConfiguration() 69 | .get(sectionKey); 70 | 71 | this.loggingService.info( 72 | `Status bar items visibility changed to: ${config}` 73 | ); 74 | 75 | if (config) { 76 | if (this.isInValid()) { 77 | this._items = [ 78 | provider.getGameFolderOpenStatusBarItem(), 79 | provider.getUnpackStatusBarItem(), 80 | provider.getCompileStatusBarItem(), 81 | provider.getOpenGameFolderButtonItem(), 82 | ]; 83 | } 84 | this.show(); 85 | } else { 86 | this.hide(); 87 | } 88 | } 89 | }) 90 | ); 91 | } 92 | 93 | dispose(): void {} 94 | 95 | show(): void { 96 | this._items?.forEach((item) => item.show()); 97 | this._gameFolderPath?.show(); 98 | } 99 | 100 | hide(): void { 101 | this._items?.forEach((item) => item.hide()); 102 | this._gameFolderPath?.hide(); 103 | } 104 | 105 | showGamePath(): void { 106 | this._gameFolderPath?.show(); 107 | } 108 | 109 | hideGamePath(): void { 110 | this._gameFolderPath?.hide(); 111 | } 112 | 113 | isInValid(): boolean { 114 | if (!this._items) return true; 115 | return this._items.length === 0; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/providers/TreeFileWatcher.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { LoggingService } from "../services/LoggingService"; 3 | import { RGSSScriptSection } from "./RGSSScriptSection"; 4 | import { LoggingMarker } from "./ScriptViewer"; 5 | 6 | export type OnDidRenameFilesProps = { 7 | oldUrl: vscode.Uri; 8 | newUrl: vscode.Uri; 9 | }; 10 | 11 | export type TreeFileWatcherEventKey = "onDidCreate" | "onDidDelete"; 12 | 13 | export class TreeFileWatcher implements vscode.Disposable { 14 | private _glob = "**/*.rb"; 15 | private _watcher?: vscode.FileSystemWatcher; 16 | 17 | private _valid: Record = { 18 | onDidCreate: true, 19 | onDidDelete: true, 20 | }; 21 | 22 | /** 23 | * 이벤트 드리븐 방식의 디커플링 패턴 24 | * `vscode.TreeDataProvider`가 아래 파일 이벤트를 구독한다. 25 | */ 26 | public onDidRenameFiles = new vscode.EventEmitter(); 27 | 28 | /** 29 | * 파일 생성 이벤트 30 | * 31 | * 트리를 info.txt로부터 다시 그릴 필요는 없지만, 기존 트리에 새로운 파일의 경로 값으로 데이터를 추가해야 한다. 32 | * 이때, Main.rb의 위쪽 또는 현재 선택된 파일의 아래쪽에 추가해야 한다. 33 | */ 34 | public onDidCreate = new vscode.EventEmitter(); 35 | 36 | /** 37 | * 파일 변경 이벤트 38 | * 39 | * 트리 변경의 필요성이 있는지 검토해야 한다. 40 | */ 41 | public onDidChange = new vscode.EventEmitter(); 42 | 43 | /** 44 | * 파일 삭제 이벤트 45 | * 46 | * 트리를 info.txt로부터 다시 그릴 필요는 없지만, 기존 트리에서 파일의 경로 값으로 데이터를 찾아 삭제 처리를 해야 한다. 47 | */ 48 | public onDidDelete = new vscode.EventEmitter(); 49 | 50 | constructor( 51 | private readonly loggingService: LoggingService, 52 | glob = "**/*.rb" 53 | ) { 54 | this._glob = glob; 55 | } 56 | 57 | create(): void { 58 | this._watcher = vscode.workspace.createFileSystemWatcher(this._glob); 59 | 60 | this.initWithEvents(); 61 | } 62 | 63 | async initWithEvents(): Promise { 64 | vscode.workspace.onDidRenameFiles((event) => { 65 | event.files.forEach((file) => { 66 | this.loggingService.info( 67 | `[INFO] change filename : ${file.oldUri} -> ${file.newUri}` 68 | ); 69 | 70 | this.onDidRenameFiles.fire({ 71 | oldUrl: file.oldUri, 72 | newUrl: file.newUri, 73 | }); 74 | }); 75 | }); 76 | 77 | this._watcher?.onDidCreate((event) => { 78 | if (this._valid.onDidCreate) { 79 | this.onDidCreate.fire(event); 80 | } 81 | }); 82 | 83 | this._watcher?.onDidChange((event) => this.onDidChange.fire(event)); 84 | 85 | this._watcher?.onDidDelete((event) => { 86 | if (this._valid.onDidDelete) { 87 | this.onDidDelete.fire(event); 88 | } 89 | }); 90 | } 91 | 92 | /** 93 | * execute the file action and ignore the watcher event when the file is created or deleted. 94 | * 95 | * @param key 96 | * @param callback 97 | */ 98 | executeFileAction( 99 | key: TreeFileWatcherEventKey, 100 | callback: () => void 101 | ): void { 102 | this._valid[key] = false; 103 | callback(); 104 | this._valid[key] = true; 105 | } 106 | 107 | dispose(): void { 108 | this.onDidRenameFiles.dispose(); 109 | this.onDidCreate.dispose(); 110 | this.onDidChange.dispose(); 111 | this.onDidDelete.dispose(); 112 | this._watcher?.dispose(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/services/ConfigService.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from "vscode"; 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | import { Path } from "../utils/Path"; 6 | import { LoggingService } from "./LoggingService"; 7 | import { Mutex } from "../Mutex"; 8 | import { RGSS } from "../RGSS"; 9 | import { JSerializeObject } from "../JSerializeObject"; 10 | 11 | /** 12 | * @class ConfigService 13 | * @description This class is responsible for managing the config file. 14 | */ 15 | export class ConfigService { 16 | /** 17 | * Gets or Sets the configuration. 18 | */ 19 | private config: RGSS.config; 20 | 21 | /** 22 | * ! ON LOAD GAME FOLDER EVENT DISPATCHER. 23 | * 24 | * Creates an event that is fired when the main game folder is changed. 25 | */ 26 | public ON_LOAD_GAME_FOLDER: vscode.EventEmitter = 27 | new vscode.EventEmitter(); 28 | 29 | /** 30 | * ! ON LOAD RGSS VERSION EVENT DISPATCHER 31 | */ 32 | private ON_LOAD_RGSS_VERSION: vscode.EventEmitter = 33 | new vscode.EventEmitter(); 34 | 35 | /** 36 | * TARGET_SCRIPT_FILE_NAME 37 | */ 38 | public static TARGET_SCRIPT_FILE_NAME = "Scripts.rvdata2"; 39 | 40 | /** 41 | * TARGET_SCRIPT_LIST_FILE_NAME 42 | */ 43 | public static TARGET_SCRIPT_LIST_FILE_NAME = "info.txt"; 44 | 45 | constructor(private readonly loggingService: LoggingService) { 46 | this.config = {}; 47 | } 48 | 49 | /** 50 | * This function is responsible for setting the main game folder to config file. 51 | * 52 | * @param gameFolder 53 | */ 54 | public async setGameFolder(gameFolder: vscode.Uri) { 55 | this.config.mainGameFolder = gameFolder; 56 | this.detectRGSSVersion(); 57 | } 58 | 59 | public static getWorkspaceValue(section: string) { 60 | const config = vscode.workspace.getConfiguration(); 61 | 62 | return config.get(section); 63 | } 64 | 65 | /** 66 | * Writes a file named "rgss-compiler.json" in the workspace folder. 67 | * 68 | * @returns 69 | */ 70 | public async saveConfig(): Promise { 71 | if (!vscode.workspace.workspaceFolders) { 72 | return vscode.window.showInformationMessage( 73 | "No folder or workspace opened" 74 | ); 75 | } 76 | 77 | const folderUri = vscode.workspace.workspaceFolders![0].uri; 78 | const fileUri = folderUri.with({ 79 | path: path.posix.join(folderUri.path, "rgss-compiler.json"), 80 | }); 81 | 82 | const buffer = new JSerializeObject({ 83 | mainGameFolder: path.posix.join(this.config.mainGameFolder?.path!), 84 | rgssVersion: this.config.rgssVersion, 85 | }).toBuffer(); 86 | 87 | await vscode.workspace.fs.writeFile(fileUri, buffer); 88 | } 89 | 90 | /** 91 | * Loads a file named "rgss-compiler.json" in the workspace folder. 92 | * 93 | * @param loggingService 94 | * @returns 95 | */ 96 | public async loadConfig( 97 | loggingService?: LoggingService 98 | ): Promise { 99 | if (!vscode.workspace.workspaceFolders) { 100 | return vscode.window.showInformationMessage( 101 | "No folder or workspace opened" 102 | ); 103 | } 104 | 105 | const folderUri = vscode.workspace.workspaceFolders![0].uri; 106 | const fileUri = folderUri.with({ 107 | path: path.posix.join(folderUri.path, "rgss-compiler.json"), 108 | }); 109 | const readData = await vscode.workspace.fs.readFile(fileUri); 110 | const jsonData = JSerializeObject.of(readData); 111 | this.config = { 112 | ...this.config, 113 | mainGameFolder: vscode.Uri.file(jsonData.mainGameFolder), 114 | }; 115 | } 116 | 117 | /** 118 | * Sets the workspace in user's visual studio code. 119 | * 120 | * @param workingFolder 121 | */ 122 | public setVSCodeWorkSpace(workingFolder: vscode.Uri) { 123 | this.config.workSpace = workingFolder; 124 | 125 | // when loading the vscode workspace, we should set initial main game folder as vscode workspace. 126 | this.setGameFolder(workingFolder); 127 | } 128 | 129 | /** 130 | * Sets the extension context. 131 | * 132 | * @param context 133 | */ 134 | public setExtensionContext(context: vscode.ExtensionContext) { 135 | this.config.extensionContext = context; 136 | } 137 | 138 | /** 139 | * Gets the extension context. 140 | * 141 | * @returns 142 | */ 143 | public getExtensionContext(): vscode.ExtensionContext { 144 | return this.config.extensionContext!; 145 | } 146 | 147 | /** 148 | * Returns the main game folder. 149 | * Note that this return type is not a string, it is a vscode.Uri type. 150 | * you should use the path.posix.join() function if you are using the path information in the vscode extension. 151 | * 152 | * @returns the main game folder. 153 | */ 154 | public getMainGameFolder(): vscode.Uri { 155 | return this.config.mainGameFolder!; 156 | } 157 | 158 | /** 159 | * Gets the workspace in user's visual studio code. 160 | */ 161 | public getVSCodeWorkSpace(): vscode.Uri { 162 | return this.config.workSpace!; 163 | } 164 | 165 | /** 166 | * Gets the configuraiton object. 167 | */ 168 | public getConfig(): RGSS.config { 169 | return this.config; 170 | } 171 | 172 | /** 173 | * Gets Ruby Game Scripting's version. 174 | * This value is one of "RGSS1", "RGSS2", "RGSS3" 175 | */ 176 | public getRGSSVersion(): RGSS.MapOfPath { 177 | return this.config.rgssVersion!; 178 | } 179 | 180 | /** 181 | * Detects the Ruby Game Scripting System version. 182 | */ 183 | public async detectRGSSVersion() { 184 | if (this.config.rgssVersion) { 185 | return this.config.rgssVersion; 186 | } 187 | 188 | const gameFolderUri = this.getMainGameFolder(); 189 | const version = { 190 | RGSS1: gameFolderUri.with({ 191 | path: path.posix.join("Data", "Scripts.rxdata"), 192 | }), 193 | RGSS2: gameFolderUri.with({ 194 | path: path.posix.join("Data", "Scripts.rvdata"), 195 | }), 196 | RGSS3: gameFolderUri.with({ 197 | path: path.posix.join("Data", "Scripts.rvdata2"), 198 | }), 199 | }; 200 | 201 | // Finds the version of RGSS (asynchronous) 202 | const mutex = new Mutex(); 203 | Array.from(["RGSS1", "RGSS2", "RGSS3"]).forEach( 204 | async (key) => { 205 | const unlock = await mutex.lock(); 206 | 207 | // File System API of Node.js didn't work in the Visual Studio Code Extension. 208 | // This is alternative behavior of the function called "fs.existsSync" 209 | try { 210 | const isValid = await vscode.workspace.fs.stat( 211 | this.getMainGameFolder().with({ 212 | path: path.posix.join( 213 | this.getMainGameFolder().path, 214 | version[key].path 215 | ), 216 | }) 217 | ); 218 | 219 | if (isValid) { 220 | this.config.rgssVersion = key; 221 | } 222 | 223 | // Occurs an event to notify the completion of processing. 224 | this.ON_LOAD_RGSS_VERSION.fire(); 225 | this.ON_LOAD_GAME_FOLDER.fire( 226 | Path.resolve(this.getMainGameFolder()) 227 | ); 228 | } catch {} 229 | unlock(); 230 | } 231 | ); 232 | 233 | // Receives the results of the event processing. 234 | this.ON_LOAD_RGSS_VERSION.event(async () => { 235 | switch (this.config.rgssVersion!) { 236 | case "RGSS1": 237 | ConfigService.TARGET_SCRIPT_FILE_NAME = "Scripts.rxdata"; 238 | break; 239 | case "RGSS2": 240 | ConfigService.TARGET_SCRIPT_FILE_NAME = "Scripts.rvdata"; 241 | break; 242 | default: 243 | case "RGSS3": 244 | ConfigService.TARGET_SCRIPT_FILE_NAME = "Scripts.rvdata2"; 245 | break; 246 | } 247 | this.loggingService.info( 248 | `RGSS Version is the same as ${this.config.rgssVersion}` 249 | ); 250 | await this.saveConfig(); 251 | }); 252 | 253 | return this.config.rgssVersion; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/services/LoggingService.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from "vscode"; 3 | import * as chalk from "chalk"; 4 | import * as dayjs from "dayjs"; 5 | 6 | const LogLevel = { 7 | info: "[INFO]", 8 | warn: "[WARN]", 9 | error: "[ERROR]", 10 | }; 11 | 12 | export class LoggingService { 13 | private outputChannel: vscode.OutputChannel = 14 | vscode.window.createOutputChannel("rgss-script-compiler"); 15 | 16 | public show() { 17 | this.outputChannel.show(); 18 | } 19 | 20 | public clear() { 21 | this.outputChannel.clear(); 22 | } 23 | 24 | /** 25 | * 커링(Currying) 기법을 사용하여 로그를 출력합니다. 26 | * @param color 27 | * @returns 28 | */ 29 | private createLog = 30 | (color: chalk.Chalk) => 31 | (level: string) => 32 | (...message: string[]) => { 33 | const time = dayjs().format("YYYY-MM-DD HH:mm:ss"); 34 | 35 | this.outputChannel.appendLine( 36 | color`${level} ${time} ::>> ${message.join(" ")}`, 37 | ); 38 | }; 39 | 40 | public info = this.createLog(chalk.white)(LogLevel.info); 41 | public warn = this.createLog(chalk.yellow)(LogLevel.warn); 42 | public error = this.createLog(chalk.red)(LogLevel.error); 43 | } 44 | -------------------------------------------------------------------------------- /src/store/GlobalStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 전역 상태를 관리하는 스토어 클래스입니다. 3 | * 이 확장에선 제가 주로 사용하는 데코레이터 기반 DI 솔루션을 사용하지 않았기 때문에 4 | * 부득이하게 다수의 객체에서 커플링 상태로 존재하는 변수들이 존재합니다. 5 | * 6 | * @class GlobalStore 7 | */ 8 | export class GlobalStore { 9 | /** 10 | * 루비 설치 여부. 11 | * 루비가 설치되어 있지 않으면 @hyrious/marshal을 사용해야 합니다. 12 | */ 13 | private isRubyInstalled: boolean; 14 | 15 | constructor() { 16 | this.isRubyInstalled = false; 17 | } 18 | 19 | /** 20 | * Ruby가 설치되어 있는지 확인합니다. 21 | * @returns 22 | */ 23 | public getIsRubyInstalled() { 24 | return this.isRubyInstalled; 25 | } 26 | 27 | /** 28 | * 루비가 설치되어 있는지 여부를 설정합니다. 29 | * @param isRubyInstalled 30 | */ 31 | public setIsRubyInstalled(isRubyInstalled: boolean) { 32 | this.isRubyInstalled = isRubyInstalled; 33 | } 34 | } 35 | 36 | export const store = new GlobalStore(); 37 | -------------------------------------------------------------------------------- /src/utils/Path.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as path from "path"; 3 | import * as vscode from "vscode"; 4 | 5 | class PathImpl { 6 | platform: NodeJS.Platform; 7 | 8 | defaultExt: string = ".rb"; 9 | 10 | constructor() { 11 | this.platform = process.platform; 12 | } 13 | 14 | /** 15 | * This method converts with the native path that can use in user's current platform, instead of vscode.Uri. 16 | * 17 | * @param url 18 | * @returns 19 | */ 20 | resolve(url: vscode.Uri): string { 21 | switch (this.platform) { 22 | case "win32": 23 | return url.fsPath; 24 | default: 25 | case "linux": 26 | case "darwin": 27 | return path.posix.join(url.path); 28 | } 29 | } 30 | 31 | getFileName(filePath: string, ext?: string | undefined) { 32 | return path.basename(filePath, ext); 33 | } 34 | 35 | getParentDirectory(filePath: string) { 36 | return path.dirname(filePath); 37 | } 38 | 39 | join(...paths: string[]) { 40 | return path.posix.join(...paths); 41 | } 42 | } 43 | 44 | export const Path = new PathImpl(); 45 | -------------------------------------------------------------------------------- /src/utils/Validator.ts: -------------------------------------------------------------------------------- 1 | export namespace Validator { 2 | export const PLASE_INPUT_SCR_NAME = "Please input a script name."; 3 | export const REMOVE_SPACE = "Please remove the space."; 4 | export const REMOVE_SPECIAL_CHARACTER = 5 | "Please remove the special characters."; 6 | export const VALID = null; 7 | export const INVALID_SCRIPT_NAME = "Cannot use this script name."; 8 | export const AVAILABLE_PLATFORMS = ["win32", "darwin", "linux"]; 9 | 10 | export function isStringOrNotEmpty(value: any): boolean { 11 | return typeof value === "string" && value.length > 0; 12 | } 13 | 14 | export function isSpace(value: string) { 15 | return value.match(/[\s]/); 16 | } 17 | 18 | export function isSpecialCharacter(value: string) { 19 | return value.match(/[\W]/); 20 | } 21 | 22 | export function isValidWindowsFilename(filename: string): boolean { 23 | const illegalCharsRegex = /[<>:"/\\|?*\x00-\x1F]/g; 24 | const reservedNames = /^(con|prn|aux|nul|com\d|lpt\d)$/i; 25 | const reservedNamesRegex = /[. ]+$/; 26 | const maxLength = 260; 27 | 28 | if (filename.length > maxLength) { 29 | return false; 30 | } 31 | 32 | if ( 33 | illegalCharsRegex.test(filename) || 34 | reservedNames.test(filename) || 35 | reservedNamesRegex.test(filename) 36 | ) { 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | export function isValidMacFilename(filename: string): boolean { 44 | const illegalCharsRegex = /[:\/]/g; 45 | const maxLength = 255; 46 | 47 | if (filename.length > maxLength) { 48 | return false; 49 | } 50 | 51 | if (illegalCharsRegex.test(filename)) { 52 | return false; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | /** 59 | * Checks whether the script name is valid or not. 60 | * 61 | * @param filename specified script name 62 | * @returns 63 | */ 64 | export function isValidScriptName(filename: string): boolean { 65 | let isValid = true; 66 | 67 | switch (process.platform) { 68 | case "win32": 69 | isValid = isValidWindowsFilename(filename); 70 | break; 71 | case "darwin": 72 | isValid = isValidMacFilename(filename); 73 | break; 74 | default: 75 | } 76 | 77 | return isValid; 78 | } 79 | 80 | export function isPlatformOK(platform: string) { 81 | return AVAILABLE_PLATFORMS.includes(platform); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export function generateUUID() { 4 | return uuidv4(); 5 | } 6 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.test.js 2 | *.js.map 3 | *.ts2 -------------------------------------------------------------------------------- /tests/example/Scripts.rvdata2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biud436/vscode-rgss-script-compiler/47e67e042af9ba871bdc06a15a1481e2d6aefee0/tests/example/Scripts.rvdata2 -------------------------------------------------------------------------------- /tests/example/Test.rb: -------------------------------------------------------------------------------- 1 | class Test 2 | def initialize 3 | @name = "hello" 4 | end 5 | def name 6 | @name 7 | end 8 | end 9 | 10 | File.open("test.dump", "w") do |f| 11 | f.write(Marshal.dump(Test.new)) 12 | end -------------------------------------------------------------------------------- /tests/example/test.dump: -------------------------------------------------------------------------------- 1 | o: Test: 2 | @nameI" 3 | hello:ET -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /types/buttons.enum.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Buttons = void 0; 4 | var Buttons; 5 | (function (Buttons) { 6 | Buttons["OK"] = "OK"; 7 | })((Buttons = exports.Buttons || (exports.Buttons = {}))); 8 | //# sourceMappingURL=buttons.enum.js.map 9 | -------------------------------------------------------------------------------- /types/buttons.enum.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"buttons.enum.js","sourceRoot":"","sources":["buttons.enum.ts"],"names":[],"mappings":";;;AAAA,IAAY,OAEX;AAFD,WAAY,OAAO;IACjB,oBAAS,CAAA;AACX,CAAC,EAFW,OAAO,GAAP,eAAO,KAAP,eAAO,QAElB"} -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "marshal" { 2 | export default class Marshal { 3 | buffer: Buffer; 4 | _index: number; 5 | 6 | constructor(data: string, encoding?: string); 7 | constructor(data: Buffer, encoding?: string); 8 | load(buffer: Buffer, encoding: string): Marshal; 9 | load(buffer: string, encoding: string): Marshal; 10 | toString(encoding: BufferEncoding): string; 11 | toJSON(): { [key: string]: any }; 12 | } 13 | } 14 | --------------------------------------------------------------------------------