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 +4 -4
- data/Gemfile +4 -2
- data/Gemfile.lock +18 -7
- data/exe/sscharter +3 -4
- data/lib/sscharter/chart.rb +10 -3
- data/lib/sscharter/cli.rb +246 -158
- data/lib/sscharter/version.rb +1 -1
- data/lib/sscharter.rb +39 -26
- data/tutorial/tutorial.md +168 -19
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5b433be7cc354c7fe69d995e2b3713c40eae03f00426b91c5823036e699fa87
|
4
|
+
data.tar.gz: 29697889f1982bef6430b43994543fc27a392752d0937dc490397821107fa98c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
10
|
-
gem
|
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.
|
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.
|
14
|
-
public_suffix (>= 2.0.2, <
|
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.
|
27
|
+
minitest (5.24.1)
|
20
28
|
module_methods (0.1.0)
|
21
|
-
public_suffix (
|
22
|
-
rake (13.1
|
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.
|
50
|
+
2.5.17
|
data/exe/sscharter
CHANGED
@@ -4,10 +4,9 @@
|
|
4
4
|
require 'sscharter'
|
5
5
|
require 'sscharter/cli'
|
6
6
|
|
7
|
-
|
8
|
-
|
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
|
12
|
+
exit subcommand.run || 0
|
data/lib/sscharter/chart.rb
CHANGED
@@ -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
|
-
}
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
34
|
-
|
35
|
-
COMMANDS = %i[init build serve]
|
56
|
+
module FilewatcherPatch
|
57
|
+
Filewatcher.prepend self
|
36
58
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
127
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
160
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
data/lib/sscharter/version.rb
CHANGED
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
|
-
|
435
|
-
|
436
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
>
|
226
|
-
>
|
227
|
-
>
|
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
|
-
>
|
230
|
-
>
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
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
|
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.
|
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-
|
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.
|
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
|