sscharter 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10b5abf2310eeeec8d15d4e26b0bb052057af5a7ec1f8831775fce9c1c824a3e
4
- data.tar.gz: edab85fb7449400947f30c182aa4c6be2ce1ba1e968a19a921fdabee4a406ba9
3
+ metadata.gz: d5b433be7cc354c7fe69d995e2b3713c40eae03f00426b91c5823036e699fa87
4
+ data.tar.gz: 29697889f1982bef6430b43994543fc27a392752d0937dc490397821107fa98c
5
5
  SHA512:
6
- metadata.gz: 31f6c6ff5a7fa1eb08af770131617d32dac68d639b63f0d5ac0f6795c0eeaed1b420d206b1c042d7f8e9975c0dc5911b88c595f668765b99e222d010ad4822da
7
- data.tar.gz: 5da054dfbf70f06d23eb9b13cccd1efd454f455e9a1fc53c709f959c354acfb2f8ec38b5f6dab164a6cc42e4cfa37612d4836342ef3f9fc2e46e52f786ec2ca3
6
+ metadata.gz: 3c89b5b04358ab75747fee2d1fb8d1aff7c42b44f99467c1a24da03acf3007df17c2cd1f4a6678ae742145a9fda74a33f21eab0effeefac0984bc0c9eccbe6e5
7
+ data.tar.gz: a336c573ee240dac57b2d3ab9bdee217fc7f46fdea145058faee8369ca27a8ae0c2503b2d9c01fd5b817288a86159c3d57be385ae25b32bc78d1e0ea2eeefa2f
data/Gemfile CHANGED
@@ -6,11 +6,13 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :develop do
9
- gem "rake", "~> 13.0"
10
- gem "minitest", "~> 5.0"
9
+ gem 'rake', '~> 13.0'
10
+ gem 'minitest', '~> 5.0'
11
11
  end
12
12
 
13
13
  gem 'rubyzip', '~> 2.3'
14
14
  gem 'launchy', '~> 2.5'
15
15
  gem 'webrick', '~> 1.8'
16
16
  gem 'filewatcher', '~> 2.0'
17
+ gem 'em-websocket', '~> 0.5'
18
+ gem 'concurrent-ruby', '~> 1.3'
data/Gemfile.lock CHANGED
@@ -1,7 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sscharter (0.5.4)
4
+ sscharter (0.6.0)
5
+ concurrent-ruby (~> 1.3)
6
+ em-websocket (~> 0.5)
5
7
  filewatcher (~> 2.0)
6
8
  launchy (~> 2.5)
7
9
  rubyzip (~> 2.3)
@@ -10,23 +12,32 @@ PATH
10
12
  GEM
11
13
  remote: https://rubygems.org/
12
14
  specs:
13
- addressable (2.8.6)
14
- public_suffix (>= 2.0.2, < 6.0)
15
+ addressable (2.8.7)
16
+ public_suffix (>= 2.0.2, < 7.0)
17
+ concurrent-ruby (1.3.3)
18
+ em-websocket (0.5.3)
19
+ eventmachine (>= 0.12.9)
20
+ http_parser.rb (~> 0)
21
+ eventmachine (1.2.7)
15
22
  filewatcher (2.1.0)
16
23
  module_methods (~> 0.1.0)
24
+ http_parser.rb (0.8.0)
17
25
  launchy (2.5.2)
18
26
  addressable (~> 2.8)
19
- minitest (5.22.3)
27
+ minitest (5.24.1)
20
28
  module_methods (0.1.0)
21
- public_suffix (5.0.4)
22
- rake (13.1.0)
29
+ public_suffix (6.0.1)
30
+ rake (13.2.1)
23
31
  rubyzip (2.3.2)
24
32
  webrick (1.8.1)
25
33
 
26
34
  PLATFORMS
35
+ x64-mingw-ucrt
27
36
  x86_64-linux
28
37
 
29
38
  DEPENDENCIES
39
+ concurrent-ruby (~> 1.3)
40
+ em-websocket (~> 0.5)
30
41
  filewatcher (~> 2.0)
31
42
  launchy (~> 2.5)
32
43
  minitest (~> 5.0)
@@ -36,4 +47,4 @@ DEPENDENCIES
36
47
  webrick (~> 1.8)
37
48
 
38
49
  BUNDLED WITH
39
- 2.5.4
50
+ 2.5.17
data/exe/sscharter CHANGED
@@ -4,10 +4,9 @@
4
4
  require 'sscharter'
5
5
  require 'sscharter/cli'
6
6
 
7
- command = ARGV.shift&.to_sym
8
- unless Sunniesnow::Charter::CLI::COMMANDS.include? command
9
- $stderr.puts "Usage: #{File.basename $0} <#{Sunniesnow::Charter::CLI::COMMANDS.join '|'}>"
7
+ unless subcommand = Sunniesnow::Charter::CLI.commands[ARGV.shift&.to_sym]
8
+ $stderr.puts "Usage: #{File.basename $0} <#{Sunniesnow::Charter::CLI.commands.keys.join '|'}>"
10
9
  exit 1
11
10
  end
12
11
 
13
- exit Sunniesnow::Charter::CLI.send(command, *ARGV) || 0
12
+ exit subcommand.run || 0
@@ -10,7 +10,7 @@ class Sunniesnow::Chart
10
10
  attr_accessor :difficulty_name, :difficulty_color, :difficulty, :difficulty_sup
11
11
  attr_reader :events
12
12
 
13
- def initialize
13
+ def initialize live_reload_port: 31108, production: false
14
14
  @title = ''
15
15
  @artist = ''
16
16
  @charter = ''
@@ -19,10 +19,12 @@ class Sunniesnow::Chart
19
19
  @difficulty = ''
20
20
  @difficulty_sup = ''
21
21
  @events = []
22
+ @live_reload_port = live_reload_port
23
+ @production = production
22
24
  end
23
25
 
24
26
  def to_json *args
25
- {
27
+ hash = {
26
28
  title: @title,
27
29
  artist: @artist,
28
30
  charter: @charter,
@@ -31,7 +33,12 @@ class Sunniesnow::Chart
31
33
  difficulty: @difficulty,
32
34
  difficultySup: @difficulty_sup,
33
35
  events: @events
34
- }.to_json
36
+ }
37
+ hash[:sscharter] = {
38
+ version: Sunniesnow::Charter::VERSION,
39
+ port: @live_reload_port
40
+ } unless @production
41
+ hash.to_json
35
42
  end
36
43
  end
37
44
 
data/lib/sscharter/cli.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
  require 'yaml'
5
5
  require 'cgi'
6
+ require 'optparse'
6
7
 
7
8
  require 'zip'
8
9
  require 'launchy'
@@ -10,191 +11,278 @@ require 'webrick'
10
11
  require 'filewatcher'
11
12
  require 'rake'
12
13
  require 'bundler'
14
+ require 'em-websocket'
15
+ require 'concurrent'
13
16
 
14
17
  require 'sscharter'
15
18
 
16
19
  module Sunniesnow
17
20
  class Charter
18
21
  module CLI
19
- end
20
- end
21
- end
22
+ module_function
23
+
24
+ def config
25
+ config_filename = File.join PROJECT_DIR, '.sscharter.yml'
26
+ config_filename = File.join PROJECT_DIR, '.sscharter.yaml' unless File.exist? config_filename
27
+ unless File.exist? config_filename
28
+ puts 'No .sscharter.yml found'
29
+ return nil
30
+ end
31
+ YAML.load_file config_filename, symbolize_names: true
32
+ end
33
+
34
+ singleton_class.attr_reader :commands
35
+ @commands = {}
22
36
 
23
- class Filewatcher
24
- # This is a hack. See:
25
- # https://github.com/filewatcher/filewatcher/blob/v2.0.0/lib/filewatcher.rb#L42
26
- # The `exit` call here will cause the WEBrick server to report a fatal error.
27
- def exit
28
- stop
37
+ class Subcommand
38
+ def initialize name, option_parser, &block
39
+ @name = name
40
+ @option_parser = option_parser
41
+ @block = block
42
+ CLI.commands[name] = self
43
+ end
44
+
45
+ def run
46
+ options = {}
47
+ @option_parser.parse! into: options
48
+ @block.(*ARGV, **options)
49
+ end
50
+ end
51
+ end
29
52
  end
30
53
  end
31
54
 
32
55
  module Sunniesnow::Charter::CLI
33
- module_function
34
-
35
- COMMANDS = %i[init build serve]
56
+ module FilewatcherPatch
57
+ Filewatcher.prepend self
36
58
 
37
- def config
38
- config_filename = '.sscharter.yml'
39
- config_filename = '.sscharter.yaml' unless File.exist? config_filename
40
- unless File.exist? config_filename
41
- puts 'No .sscharter.yml found'
42
- return nil
59
+ # This is a hack. See:
60
+ # https://github.com/filewatcher/filewatcher/blob/v2.0.0/lib/filewatcher.rb#L42
61
+ # The `exit` call here will cause the WEBrick server to report a fatal error.
62
+ def exit
63
+ stop
43
64
  end
44
- YAML.load_file config_filename, symbolize_names: true
45
65
  end
46
66
 
47
- def init project_dir, *files
48
- if File.directory? project_dir
49
- puts "Directory #{project_dir} already exists"
50
- return 1
51
- end
52
- files_dir = File.join project_dir, 'files'
53
- FileUtils.mkdir_p files_dir
54
- FileUtils.cp_r files, files_dir
55
- FileUtils.cd project_dir do
56
- File.write 'Gemfile', <<~GEMFILE
57
- # frozen_string_literal: true
58
- source 'https://rubygems.org'
59
- gem 'sscharter', '~> #{Sunniesnow::Charter::VERSION}'
60
- gem 'rake', '~> #{Rake::VERSION}'
61
- gem 'bundler', '~> #{Bundler::VERSION}'
62
- GEMFILE
63
- File.write 'Rakefile', <<~RAKEFILE
64
- # frozen_string_literal: true
65
- task default: :build
66
- task :build do
67
- exec 'bundle exec sscharter build'
68
- end
69
- task :serve do
70
- exec 'bundle exec sscharter serve'
71
- end
72
- RAKEFILE
73
- File.write '.gitignore', <<~GITIGNORE
74
- /.bundle/
75
- /tmp/
76
- /build/
77
- GITIGNORE
78
- File.write '.sscharter.yml', <<~SSCHARTER
79
- ---
80
- project_name: #{File.basename project_dir}
81
- build_dir: build
82
- files_dir: files
83
- sources_dir: src
84
- include:
85
- - README.md
86
- SSCHARTER
87
- File.write 'README.md', <<~README
88
- # #{File.basename project_dir}
89
-
90
- <!-- TODO: Write a description for your project here -->
91
-
92
- ## Building
93
-
94
- [Install Ruby (>= 3.0.0)](https://www.ruby-lang.org/en/documentation/installation/),
95
- and then run `rake`.
96
- The built chart will be `build/#{File.basename project_dir}.ssc`.
97
-
98
- ## Legal status
99
-
100
- <!-- The artist should have explicitly stated publicly that they permit charting the music,
101
- or have contacted you to give proper permission.
102
- Whether or not, you should state clearly the legal status of the chart here. -->
103
- README
104
- FileUtils.mkdir_p 'src'
105
- File.write 'src/master.rb', <<~CHART
106
- # frozen_string_literal: true
107
-
108
- Sunniesnow::Charter.open 'master' do
109
-
110
- title 'The title of the music'
111
- artist 'The artist of the music'
112
- charter 'Your name'
113
- difficulty_name 'Master'
114
- difficulty_color '#8c68f3'
115
- difficulty '12'
116
-
117
- offset 0
118
- bpm 120
119
-
120
- tp_chain 0, 0, 1 do
121
- t -50, 0, 'hello'
122
- b 1 # proceed by 1 beat
123
- t 50, 0, 'world'
124
- end
67
+ module OptionParserPatch
68
+ OptionParser.prepend self
125
69
 
126
- end
127
- CHART
70
+ def order!(argv = default_argv, into: nil, **keywords, &nonopt)
71
+ setter = ->(name, val) {into[name.tr(?-, ?_).to_sym] = val} if into
72
+ parse_in_order(argv, setter, **keywords, &nonopt)
128
73
  end
129
- puts "Project initialized at #{project_dir}"
130
74
  end
75
+ end
76
+
77
+ option_parser = OptionParser.new do |o|
78
+ o.banner = 'Usage: sscharter init [project_dir] [files...]'
79
+ end
80
+ Sunniesnow::Charter::CLI::Subcommand.new :init, option_parser do |project_dir = Sunniesnow::Charter::PROJECT_DIR, *files|
81
+ if File.directory?(project_dir) && !Dir.empty?(project_dir)
82
+ puts "Directory #{project_dir} already exists and is not empty"
83
+ return 1
84
+ end
85
+ files_dir = File.expand_path File.join project_dir, 'files'
86
+ FileUtils.mkdir_p files_dir
87
+ FileUtils.cp_r files, files_dir
88
+ FileUtils.cd project_dir do
89
+ File.write 'Gemfile', <<~GEMFILE
90
+ # frozen_string_literal: true
91
+ source 'https://rubygems.org'
92
+ gem 'sscharter', '~> #{Sunniesnow::Charter::VERSION}'
93
+ gem 'rake', '~> #{Rake::VERSION}'
94
+ gem 'bundler', '~> #{Bundler::VERSION}'
95
+ GEMFILE
96
+ File.write 'Rakefile', <<~RAKEFILE
97
+ # frozen_string_literal: true
98
+ task default: :build
99
+ task :build do
100
+ exec 'bundle exec sscharter build'
101
+ end
102
+ task :serve do
103
+ exec 'bundle exec sscharter serve'
104
+ end
105
+ RAKEFILE
106
+ File.write '.gitignore', <<~GITIGNORE
107
+ /.bundle/
108
+ /tmp/
109
+ /build/
110
+ GITIGNORE
111
+ File.write '.sscharter.yml', <<~SSCHARTER
112
+ ---
113
+ project_name: #{File.basename project_dir}
114
+ build_dir: build
115
+ files_dir: files
116
+ sources_dir: src
117
+ include:
118
+ - README.md
119
+ SSCHARTER
120
+ File.write 'README.md', <<~README
121
+ # #{File.basename project_dir}
122
+
123
+ <!-- TODO: Write a description for your project here -->
124
+
125
+ ## Building
126
+
127
+ [Install Ruby (>= 3.0.0)](https://www.ruby-lang.org/en/documentation/installation/),
128
+ and then run `rake`.
129
+ The built chart will be `build/#{File.basename project_dir}.ssc`.
130
+
131
+ ## Legal status
132
+
133
+ <!-- The artist should have explicitly stated publicly that they permit charting the music,
134
+ or have contacted you to give proper permission.
135
+ Whether or not, you should state clearly the legal status of the chart here. -->
136
+ README
137
+ FileUtils.mkdir_p 'src'
138
+ File.write 'src/master.rb', <<~CHART
139
+ # frozen_string_literal: true
140
+
141
+ Sunniesnow::Charter.open 'master' do
142
+
143
+ title 'The title of the music'
144
+ artist 'The artist of the music'
145
+ charter 'Your name'
146
+ difficulty_name 'Master'
147
+ difficulty_color '#8c68f3'
148
+ difficulty '12'
149
+
150
+ offset 0
151
+ bpm 120
152
+
153
+ tp_chain 0, 0, 1 do
154
+ t -50, 0, 'hello'
155
+ b 1 # proceed by 1 beat
156
+ t 50, 0, 'world'
157
+ end
158
+
159
+ end
160
+ CHART
161
+ end
162
+ puts "Project initialized at #{project_dir}"
163
+ end
131
164
 
132
- def build
133
- return 1 unless config = self.config
134
- project_name = config[:project_name] || File.basename(Dir.pwd)
135
- build_dir = config[:build_dir] || 'build'
136
- files_dir = config[:files_dir] || 'files'
137
- sources_dir = config[:sources_dir] || 'src'
138
- include_files = config[:include] || []
139
- ::Sunniesnow::Charter.charts.clear
140
- Dir.glob File.join sources_dir, '*.rb' do |filename|
141
- load filename
142
- rescue Exception => e
143
- puts "Error loading #{filename}:"
144
- puts e.full_message
145
- return 1
165
+ def build **opts
166
+ return 1 unless config = Sunniesnow::Charter::CLI.config
167
+ dir = Sunniesnow::Charter::PROJECT_DIR
168
+ project_name = config[:project_name] || File.basename(dir)
169
+ build_dir = File.join dir, config[:build_dir] || 'build'
170
+ files_dir = File.join dir, config[:files_dir] || 'files'
171
+ sources_dir = File.join dir, config[:sources_dir] || 'src'
172
+ include_files = (config[:include] || []).map { File.join dir, _1 }
173
+ Sunniesnow::Charter.charts.clear
174
+ Dir.glob File.join sources_dir, '*.rb' do |filename|
175
+ load filename
176
+ rescue Exception => e
177
+ puts "Error loading #{filename}:"
178
+ puts e.full_message
179
+ return 1
180
+ end
181
+ FileUtils.mkdir_p build_dir
182
+ build_filename = File.join build_dir, "#{project_name}.ssc"
183
+ FileUtils.rm build_filename if File.exist? build_filename
184
+ Zip::File.open build_filename, create: true do |zip_file|
185
+ Dir.glob File.join files_dir, '**', '*' do |filename|
186
+ zip_file.add filename["#{files_dir}/".length..], filename
146
187
  end
147
- FileUtils.mkdir_p build_dir
148
- build_filename = File.join build_dir, "#{project_name}.ssc"
149
- FileUtils.rm build_filename if File.exist? build_filename
150
- Zip::File.open build_filename, create: true do |zip_file|
151
- Dir.glob File.join files_dir, '**', '*' do |filename|
152
- zip_file.add filename["#{files_dir}/".length..], filename
188
+ include_files.each do |pattern|
189
+ Dir.glob pattern do |filename|
190
+ zip_file.add filename["#{dir}/".length..], filename
153
191
  end
154
- include_files.each do |pattern|
155
- Dir.glob pattern do |filename|
156
- zip_file.add filename, filename
157
- end
192
+ end
193
+ Sunniesnow::Charter.charts.each do |name, chart|
194
+ begin
195
+ output = chart.output_json **opts
196
+ rescue => e
197
+ puts 'An error happened. Report if this is a bug of sscharter.'
198
+ puts e.full_message
199
+ return 2
158
200
  end
159
- ::Sunniesnow::Charter.charts.each do |name, chart|
160
- begin
161
- output = chart.output_json
162
- rescue => e
163
- puts 'An error happened. Report if this is a bug of sscharter.'
164
- puts e.full_message
165
- return 2
166
- end
167
- zip_file.get_output_stream "#{name}.json" do |file|
168
- file.write chart.output_json
169
- end
201
+ zip_file.get_output_stream "#{name}.json" do |file|
202
+ file.write output
170
203
  end
171
204
  end
172
- 0
173
205
  end
206
+ 0
207
+ end
174
208
 
175
- def serve port = 8011
176
- port = port.to_i
177
- config = self.config
178
- server = WEBrick::HTTPServer.new Port: port, DocumentRoot: config[:build_dir]
179
- def server.service request, response
180
- super
181
- response['Access-Control-Allow-Origin'] = '*'
182
- response['Cache-Control'] = 'no-cache'
183
- response['Content-Type'] = 'application/zip' if request.path.end_with? '.ssc'
184
- end
185
- url = CGI.escape "http://localhost:#{port}/#{config[:project_name]}.ssc"
186
- filewatcher = Filewatcher.new [config[:files_dir], config[:sources_dir], *config[:include]]
187
- Launchy.open "https://sunniesnow.github.io/game/?level-file=online&level-file-online=#{url}"
188
- filewatcher_thread = Thread.new do
189
- puts 'Building...'
190
- puts build == 0 ? 'Finished' : 'Failed'
191
- filewatcher.watch do |changes|
192
- puts 'Rebuilding...'
193
- puts build == 0 ? 'Finished' : 'Failed'
209
+ option_parser = OptionParser.new do |o|
210
+ o.banner = 'Usage: sscharter build'
211
+ end
212
+ Sunniesnow::Charter::CLI::Subcommand.new :build, option_parser do
213
+ build production: true
214
+ end
215
+
216
+ option_parser = OptionParser.new do |o|
217
+ o.banner = 'Usage: sscharter serve [options]'
218
+ o.on '--host=HOST', String, 'Host name'
219
+ o.on '--exposed-host=HOST', String, 'Exposed host name'
220
+ o.on '--port=PORT', Integer, 'Port number'
221
+ o.on '--live-reload-port=PORT', Integer, 'live reload port number'
222
+ o.on '--[no-]production', 'Disable live reload'
223
+ o.on '--[no-]open-browser', 'Open browser'
224
+ end
225
+ Sunniesnow::Charter::CLI::Subcommand.new :serve, option_parser do |host: '0.0.0.0', exposed_host: 'localhost', port: 8011, live_reload_port: 31108, production: false, open_browser: true|
226
+ return 1 unless config = Sunniesnow::Charter::CLI.config
227
+ dir = Sunniesnow::Charter::PROJECT_DIR
228
+ project_name = config[:project_name] || File.basename(dir)
229
+ build_dir = File.join dir, config[:build_dir] || 'build'
230
+ files_dir = File.join dir, config[:files_dir] || 'files'
231
+ sources_dir = File.join dir, config[:sources_dir] || 'src'
232
+ include_files = (config[:include] || []).map { File.join dir, _1 }
233
+ server = WEBrick::HTTPServer.new BindAddress: host, Port: port, DocumentRoot: build_dir
234
+ def server.service request, response
235
+ super
236
+ response['Access-Control-Allow-Origin'] = '*'
237
+ response['Cache-Control'] = 'no-cache'
238
+ response['Content-Type'] = 'application/zip' if request.path.end_with? '.ssc'
239
+ end
240
+ unless production
241
+ live_reload_clients = Concurrent::Array.new
242
+ Thread.new do
243
+ EM.run do
244
+ EM::WebSocket.run host:, port: live_reload_port do |ws|
245
+ ws.onopen do
246
+ live_reload_clients.push ws
247
+ end
248
+ ws.onclose do
249
+ live_reload_clients.delete ws
250
+ end
251
+ ws.onmessage do |message|
252
+ data = JSON.parse message, symbolize_names: true
253
+ case data[:type]
254
+ when 'connect'
255
+ puts "Connected: #{data[:userAgent]}"
256
+ when 'eventInfoTip'
257
+ if backtrace = Sunniesnow::Charter.charts[File.basename data[:chart], '.*']&.events[data[:id]]&.backtrace
258
+ puts "Event #{data[:id]} in #{data[:chart]} was defined at"
259
+ puts backtrace
260
+ else
261
+ puts "Event #{data[:id]} in #{data[:chart]} is not found"
262
+ end
263
+ else
264
+ puts "Unknown message type '#{data[:type]}' from live reload client"
265
+ end
266
+ end
267
+ end
194
268
  end
195
- server.shutdown
196
269
  end
197
- server.start
198
- 0
199
270
  end
271
+ url = "http://#{exposed_host}:#{port}/#{project_name}.ssc"
272
+ filewatcher = Filewatcher.new [files_dir, sources_dir, *include_files]
273
+ Launchy.open "https://sunniesnow.github.io/game/?level-file=online&level-file-online=#{CGI.escape url}" if open_browser
274
+ build_proc = ->is_first do
275
+ puts is_first ? 'Building...' : 'Rebuilding...'
276
+ success = build(live_reload_port:, production:) == 0
277
+ puts success ? is_first ? "Finished; access at #{url}" : 'Finished' : 'Failed'
278
+ live_reload_clients.each { _1.send JSON.generate type: 'update' } unless production
279
+ end
280
+ filewatcher_thread = Thread.new do
281
+ build_proc.(true)
282
+ filewatcher.watch { |changes| build_proc.(false) }
283
+ server.shutdown
284
+ EM.stop unless production
285
+ end
286
+ server.start
287
+ 0
200
288
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sunniesnow
4
4
  class Charter
5
- VERSION = "0.5.4"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/sscharter.rb CHANGED
@@ -6,6 +6,8 @@ require_relative 'sscharter/chart'
6
6
 
7
7
  class Sunniesnow::Charter
8
8
 
9
+ PROJECT_DIR = File.expand_path(ENV['SSCHARTER_PROJECT_DIR'] ||= Dir.pwd)
10
+
9
11
  using Sunniesnow::Utils
10
12
 
11
13
  class OffsetError < StandardError
@@ -71,7 +73,7 @@ class Sunniesnow::Charter
71
73
  TIP_POINTABLE_TYPES = %i[tap hold flick drag]
72
74
 
73
75
  attr_accessor :beat, :offset, :duration_beats, :properties
74
- attr_reader :type, :bpm_changes
76
+ attr_reader :type, :bpm_changes, :backtrace
75
77
 
76
78
  def initialize type, beat, duration_beats = nil, bpm_changes, **properties
77
79
  @beat = beat
@@ -80,6 +82,7 @@ class Sunniesnow::Charter
80
82
  @bpm_changes = bpm_changes
81
83
  @properties = properties
82
84
  @offset = 0.0
85
+ @backtrace = caller.filter { _1.sub! /^#{PROJECT_DIR}\//, '' }
83
86
  end
84
87
 
85
88
  def time_at_relative_beat delta_beat
@@ -118,6 +121,11 @@ class Sunniesnow::Charter
118
121
  def tip_pointable?
119
122
  TIP_POINTABLE_TYPES.include? @type
120
123
  end
124
+
125
+ def inspect
126
+ "#<#@type at #@beat#{@duration_beats && " for #@duration_beats"} offset #@offset: " +
127
+ @properties.map { |k, v| "#{k}=#{v.inspect}" }.join(', ') + '>'
128
+ end
121
129
  end
122
130
 
123
131
  # Implements homography
@@ -304,6 +312,8 @@ class Sunniesnow::Charter
304
312
  singleton_class.attr_reader :charts
305
313
  @charts = {}
306
314
 
315
+ attr_reader :events
316
+
307
317
  def self.open name, &block
308
318
  result = @charts[name] ||= new name
309
319
  result.instance_eval &block if block
@@ -333,6 +343,7 @@ class Sunniesnow::Charter
333
343
  @bpm_changes = nil
334
344
  @tip_point_mode_stack = [:none]
335
345
  @current_tip_point_stack = []
346
+ @current_tip_point_group_stack = []
336
347
  @tip_point_peak = 0
337
348
  @current_duplicate = 0
338
349
  @tip_point_start_to_add_stack = [nil]
@@ -431,26 +442,12 @@ class Sunniesnow::Charter
431
442
  @bpm_changes.time_at beat
432
443
  end
433
444
 
434
- def tip_point_chain *args, preserve_beat: true, **opts, &block
435
- tip_point :chain, *args, **opts do
436
- group preserve_beat: preserve_beat, &block
437
- end#.tap { @tip_point_peak += 1 }
438
- end
439
- alias tp_chain tip_point_chain
440
-
441
- def tip_point_drop *args, preserve_beat: true, **opts, &block
442
- tip_point :drop, *args, **opts do
443
- group preserve_beat: preserve_beat, &block
445
+ %i[chain drop none].each do |mode|
446
+ define_method "tip_point_#{mode}" do |*args, **opts, &block|
447
+ tip_point mode, *args, **opts, &block
444
448
  end
449
+ alias_method "tp_#{mode}", "tip_point_#{mode}"
445
450
  end
446
- alias tp_drop tip_point_drop
447
-
448
- def tip_point_none preserve_beat: true, &block
449
- tip_point :none do
450
- group preserve_beat: preserve_beat, &block
451
- end
452
- end
453
- alias tp_none tip_point_none
454
451
 
455
452
  def group preserve_beat: true, &block
456
453
  raise ArgumentError, 'no block given' unless block
@@ -470,7 +467,7 @@ class Sunniesnow::Charter
470
467
  result
471
468
  end
472
469
 
473
- def tip_point mode, *args, **opts, &block
470
+ def tip_point mode, *args, preserve_beat: true, **opts, &block
474
471
  @tip_point_mode_stack.push mode
475
472
  if mode == :none
476
473
  @current_tip_point_stack.push nil
@@ -479,13 +476,21 @@ class Sunniesnow::Charter
479
476
  @current_tip_point_stack.push @tip_point_peak
480
477
  @tip_point_peak += 1
481
478
  end
482
- result = block.()
479
+ result = group preserve_beat: do
480
+ @current_tip_point_group_stack.push @groups.last
481
+ instance_eval &block
482
+ end
483
483
  @tip_point_start_to_add_stack.pop
484
484
  @tip_point_mode_stack.pop
485
485
  @current_tip_point_stack.pop
486
+ @current_tip_point_group_stack.pop
486
487
  result
487
488
  end
488
489
 
490
+ def remove *events
491
+ events.each { @events.delete _1 }
492
+ end
493
+
489
494
  def event type, duration_beats = nil, **properties
490
495
  raise OffsetError.new __method__ unless @bpm_changes
491
496
  event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
@@ -508,7 +513,11 @@ class Sunniesnow::Charter
508
513
  def push_tip_point_start start_event
509
514
  start_event[:tip_point] = @current_tip_point_stack.last.to_s
510
515
  tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
511
- @groups.each { _1.push tip_point_start } if tip_point_start
516
+ return unless tip_point_start
517
+ @groups.each do |group|
518
+ group.push tip_point_start
519
+ break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
520
+ end
512
521
  end
513
522
 
514
523
  def transform events, &block
@@ -629,8 +638,8 @@ class Sunniesnow::Charter
629
638
  end
630
639
  end
631
640
 
632
- def to_sunniesnow
633
- result = Sunniesnow::Chart.new
641
+ def to_sunniesnow **opts
642
+ result = Sunniesnow::Chart.new **opts
634
643
  result.title = @title
635
644
  result.artist = @artist
636
645
  result.charter = @charter
@@ -642,8 +651,12 @@ class Sunniesnow::Charter
642
651
  result
643
652
  end
644
653
 
645
- def output_json
646
- to_sunniesnow.to_json
654
+ def output_json **opts
655
+ to_sunniesnow(**opts).to_json
656
+ end
657
+
658
+ def inspect
659
+ "#<Sunniesnow::Charter #@name>"
647
660
  end
648
661
 
649
662
  end
data/tutorial/tutorial.md CHANGED
@@ -212,38 +212,171 @@ This command will open the Sunniesnow webpage in your browser for you.
212
212
  The `online` field of Sunniesnow is already filled with the address to the generated level file.
213
213
  Every time you save changes to the source codes,
214
214
  the program will automatically rebuild the level file.
215
- You just need to hit "load" again to reload the level file.
215
+ Sunniesnow will reload the level file automatically if you are using the default settings.
216
216
 
217
217
  The port of the local server is 8011 by default.
218
218
  If you need to change the port to 1314 for example, you need to run
219
219
 
220
220
  ```shell
221
- bundle exec sscharter serve 1314
221
+ bundle exec sscharter serve --port 1314
222
222
  ```
223
223
 
224
+ You can edit `Rakefile` to make `rake serve` use that port.
225
+
224
226
  > [!TIP]
225
- > The default settings of Sunniesnow are tuned for gameplay instead of charting,
226
- > so you may want to change some settings to help you write the chart.
227
- > Here are some useful settings:
227
+ > If you do not even want the live reload feature,
228
+ > you can turn it off by using
229
+ >
230
+ > ```shell
231
+ > bundle exec sscharter serve --production
232
+ > ```
228
233
  >
229
- > - Enable [`autoplay`](https://sunniesnow.github.io/game/help.html#autoplay).
230
- > - Disable [`fullscreen-on-start`](https://sunniesnow.github.io/game/help.html#fullscreen-on-start).
231
- > Fullscreen is good for gamplay but is disturbing for charting.
232
- > - Enable [`debug`](https://sunniesnow.github.io/game/help.html#debug),
233
- > and you can see the current progress of the music, the judgement region of notes,
234
- > the coordinates of where you click your mouse, etc.
235
- > - Use the option [`start`](https://sunniesnow.github.io/game/help.html#start)
236
- > to start the music at a certain time.
237
- > You can use the progress you got from debug mode UI to set it.
238
- > - Enable [`se-with-music`](https://sunniesnow.github.io/game/help.html#se-with-music)
239
- > and set [`chart-offset`](https://sunniesnow.github.io/game/help.html#chart-offset) to zero.
240
- > This will make sure the timing of sound effects is precise no matter how much latency your computer has.
234
+ > This way, the level file generated is the same as that generated by `sscharter build`,
235
+ > which does not contain the info necessary for live reload.
236
+
237
+ ### Configure Sunniesnow to suit charting
238
+
239
+ The default settings of Sunniesnow are tuned for gameplay instead of charting,
240
+ so you may want to change some settings to help you write the chart.
241
+ Use this link to set these settings quickly:
242
+
243
+ https://sunniesnow.github.io/game/?volume-se=1&se-with-music=true&chart-offset=0&autoplay=true&progress-adjustable=true&hide-pause-ui=true&resume-preparation-time=0&always-update-fx=true&debug=true&fullscreen-on-start=false&sscharter=true&sscharter-live-restart=true
244
+
245
+ First, the most important part of rhythm game charting is to ensure that notes are pefectly aligned with the music.
246
+ To help you with that, configure the following settings:
247
+
248
+ - set [`volume-se`](https://sunniesnow.github.io/game/help.html#volume-se) to a proper positive value,
249
+ - enable [`se-with-music`](https://sunniesnow.github.io/game/help.html#se-with-music), and
250
+ - set [`chart-offset`](https://sunniesnow.github.io/game/help.html#chart-offset) to `0` (this is default).
251
+
252
+ You can still set [`delay`](https://sunniesnow.github.io/game/help.html#delay)
253
+ and [`offset`](https://sunniesnow.github.io/game/help.html#offset)
254
+ to fit the audio and display latency of your device.
255
+ You can still ensure the notes are aligned with the music by listening to the sound effects.
256
+ To learn what the differences between these two settings and `chart-offset`,
257
+ read [differences between different offsets](https://sunniesnow.github.io/game/help.html#differences-between-different-offsets).
258
+
259
+ Configure the following settings to have a video-player-like experience:
260
+
261
+ - enable [`autoplay`](https://sunniesnow.github.io/game/help.html#autoplay),
262
+ - enable [`progress-adjustable`](https://sunniesnow.github.io/game/help.html#progress-adjustable),
263
+ - enable [`hide-pause-ui`](https://sunniesnow.github.io/game/help.html#hide-pause-ui),
264
+ - enable [`always-update-fx`](https://sunniesnow.github.io/game/help.html#always-update-fx), and
265
+ - set [`resume-preparation-time`](https://sunniesnow.github.io/game/help.html#resume-preparation-time) to `0`.
266
+
267
+ You can drag the progress bar or hit <kbd>ArrowLeft</kbd>
268
+ or <kbd>ArrowRight</kbd> to adjust the progress.
269
+ See the help contents of [`progress-adjustable`](https://sunniesnow.github.io/game/help.html#progress-adjustable)
270
+ to learn other controls available in this mode.
271
+
272
+ Enable [`debug`](https://sunniesnow.github.io/game/help.html#debug),
273
+ and you can see the current progress of the music, the judgement region of notes,
274
+ the coordinates of where you click your mouse, etc.
275
+ You can also pin coordinates on the screen by clicking while holding <kbd>Ctrl</kbd>.
276
+ Other controls for pinning coordinates in this mode are introduced in the help contents of
277
+ [`debug`](https://sunniesnow.github.io/game/help.html#debug).
278
+ When the game is paused in debug mode, you can also click
279
+ a note, background note, or background pattern to reveal the event details,
280
+ including the event ID, its time and properties, and (if it is a note) which combo number it is at.
281
+ The most useful feature of the debug mode for charting with sscharter is that,
282
+ if the chart you are playing is served by sscharter (without the `--production` flag),
283
+ the sscharter server will tell you (in the terminal) where the event is defined
284
+ when you reveal the event details in Sunniesnow.
285
+ This gives the ability of reverse searching.
286
+
287
+ Finally, although it is already convenient to have the level file
288
+ reloaded automatically when you save changes to the source codes,
289
+ you can enable [`sscharter-live-restart`](https://sunniesnow.github.io/game/help.html#sscharter-live-restart)
290
+ to make it more convenient.
291
+ This setting will make the game restarted automatically when you save changes to the source codes.
292
+
293
+ Other useful settings:
294
+
295
+ - Disable [`fullscreen-on-start`](https://sunniesnow.github.io/game/help.html#fullscreen-on-start).
296
+ Fullscreen is good for gamplay but is disturbing for charting.
297
+ - Use the option [`start`](https://sunniesnow.github.io/game/help.html#start)
298
+ to start the music at a certain time.
299
+ You can use the progress you got from debug mode UI to set it.
300
+
301
+ ### Text editor configuration
302
+
303
+ The workflow can be enhanced with a good text editor.
304
+ Here is a setup with Visual Studio Code
305
+ (abbreviated as VS Code in the following) without any external extensions.
306
+
307
+ Create a file `.vscode/tasks.json` in your project and write:
308
+
309
+ ```json
310
+ {
311
+ "version": "2.0.0",
312
+ "tasks": [
313
+ {
314
+ "label": "Serve",
315
+ "type": "shell",
316
+ "command": "rake serve",
317
+ "presentation": {
318
+ "reveal": "always"
319
+ }
320
+ },
321
+ {
322
+ "label": "Build",
323
+ "type": "shell",
324
+ "command": "rake build",
325
+ "group": "build"
326
+ }
327
+ ]
328
+ }
329
+ ```
330
+
331
+ Then, edit `Rakefile` to disable browser launching and enable live restart:
332
+
333
+ ```ruby
334
+ task :serve do
335
+ exec 'bundle exec sscharter serve --no-open-browser --live-restart'
336
+ end
337
+ ```
338
+
339
+ Hit <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> in VS Code,
340
+ type "Simple Browser", and you will see an option to open a webpage using
341
+ the simple browser feature of VS Code.
342
+ Select that option, and then there is a popup telling you to enter the web address.
343
+ Type "https://sunniesnow.github.io/game"
344
+ (or the very long link that sets all the settings for charting quickly shown
345
+ [above](#configure-sunniesnow-to-suit-charting)) and then enter.
346
+ After that, drag the simple browser tab to make it alongside with the main editor
347
+ (the split editor feature of VS Code).
348
+
349
+ Then, hit <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> and type "Run Task".
350
+ There is an option with which you can run a task configured in `.vscode/tasks.json`.
351
+ Select that option, and then select the "Serve" task.
352
+ (After that, VS Code asks you whether to scan the output. You can skip that.)
353
+ A new widget will open in the bottom (the integrated terminal feature of VS Code)
354
+ showing the terminal output of sscharter.
355
+ After sscharter finishes building the level file, a URL to the level file will be shown.
356
+ Copy that URL into the
357
+ [`level-file-online`](https://sunniesnow.github.io/game/help.html#level-file-online) setting.
358
+ Then, hit the start button in Sunniesnow to check that you can play the chart.
359
+
360
+ After that, configure Sunniesnow to suit charting as described
361
+ [above](#configure-sunniesnow-to-suit-charting).
362
+ You now have a decent charting setup.
363
+
364
+ When you want to reverse search an event, you can pause the game in debug mode
365
+ and click the event (note, background note, or background pattern).
366
+ You can then see the its definition location in the terminal output below.
367
+ In VS Code, you can navigate to the location in the editor
368
+ by clicking the file path and line number in the terminal output
369
+ while holding <kbd>Ctrl</kbd> (or other modifier keys depending on your configuration).
370
+
371
+ The following is a screenshot of the setup:
372
+
373
+ ![VS Code setup](https://i.imgur.com/O3nhKX4.png)
241
374
 
242
375
  ## What does each line in `src/master.rb` mean?
243
376
 
244
377
  Now, you are ready to write the chart!
245
378
  Open `src/master.rb` using your text editor.
246
- Here I explain what does each line mean.
379
+ Here I explain what each line in this file means.
247
380
 
248
381
  ```ruby
249
382
  Sunniesnow::Charter.open 'master' do
@@ -271,7 +404,7 @@ difficulty_sup '+' # optional
271
404
  ```
272
405
 
273
406
  These lines are the metadata of the chart.
274
- They already explains pretty much themselves.
407
+ They already explain pretty much themselves.
275
408
  Just fill them in!
276
409
 
277
410
  By the way, there is a trick about the `difficulty_color` for your convenience.
@@ -1064,6 +1197,22 @@ TODO.
1064
1197
 
1065
1198
  ### Use Git as a version manager
1066
1199
 
1200
+ You ever want to keep track of the changes you made to the chart?
1201
+ You ever wish to revert to a previous version of the chart?
1202
+ You ever want to collaborate with others on the chart?
1203
+ You can use [Git](https://git-scm.com/) to implement these version control features.
1204
+
1205
+ First, install Git.
1206
+ Then, run `git init` in your project directory to make it a Git repository.
1207
+ Every time you want to save the current version of the chart,
1208
+ you can run `git add .` to stage all the changes,
1209
+ and then run `git commit -m "Your commit message here"` to commit the changes.
1210
+ Git may prompt you to set up your name and email address,
1211
+ then just do so by following the instructions.
1212
+
1213
+ A detailed tutorial on how to use Git is beyond the scope of this tutorial,
1214
+ so I may just refer you to the official [tutorial](https://git-scm.com/docs/gittutorial) of Git.
1215
+
1067
1216
  ### Useful loops
1068
1217
 
1069
1218
  ### Homography
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sscharter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ulysses Zhan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-23 00:00:00.000000000 Z
11
+ date: 2024-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: em-websocket
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.5'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: concurrent-ruby
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: minitest
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -135,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
163
  - !ruby/object:Gem::Version
136
164
  version: '0'
137
165
  requirements: []
138
- rubygems_version: 3.4.22
166
+ rubygems_version: 3.5.9
139
167
  signing_key:
140
168
  specification_version: 4
141
169
  summary: A Ruby DSL for writing Sunniesnow charts