sscharter 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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
|