├── .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 | 
16 | 
17 | 
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 |
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 |
--------------------------------------------------------------------------------