tyrano_dsl 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +290 -0
- data/Rakefile +10 -0
- data/bin/setup +8 -0
- data/bin/tyrano_dsl.rb +21 -0
- data/lib/tyrano_dsl/elements/background.rb +31 -0
- data/lib/tyrano_dsl/elements/character.rb +49 -0
- data/lib/tyrano_dsl/elements/elements_module.rb +7 -0
- data/lib/tyrano_dsl/elements/jump_target.rb +18 -0
- data/lib/tyrano_dsl/elements/label.rb +19 -0
- data/lib/tyrano_dsl/elements/scene.rb +21 -0
- data/lib/tyrano_dsl/elements/stance.rb +26 -0
- data/lib/tyrano_dsl/elements/title_screen.rb +16 -0
- data/lib/tyrano_dsl/elements/variable.rb +22 -0
- data/lib/tyrano_dsl/elements/world.rb +62 -0
- data/lib/tyrano_dsl/elements_writers/background_writer.rb +30 -0
- data/lib/tyrano_dsl/elements_writers/character_writer.rb +32 -0
- data/lib/tyrano_dsl/elements_writers/characters_writer.rb +43 -0
- data/lib/tyrano_dsl/elements_writers/elements_writers_module.rb +27 -0
- data/lib/tyrano_dsl/elements_writers/scene_writer.rb +30 -0
- data/lib/tyrano_dsl/elements_writers/title_screen_writer.rb +87 -0
- data/lib/tyrano_dsl/elements_writers/variables_writer.rb +30 -0
- data/lib/tyrano_dsl/file_actions/clear_directory.rb +34 -0
- data/lib/tyrano_dsl/file_actions/create_file.rb +33 -0
- data/lib/tyrano_dsl/file_actions/file_copy.rb +36 -0
- data/lib/tyrano_dsl/file_actions/files_actions_module.rb +28 -0
- data/lib/tyrano_dsl/file_actions/json_patch.rb +47 -0
- data/lib/tyrano_dsl/main.rb +18 -0
- data/lib/tyrano_dsl/parsed_word.rb +22 -0
- data/lib/tyrano_dsl/parser.rb +58 -0
- data/lib/tyrano_dsl/parsing_context.rb +18 -0
- data/lib/tyrano_dsl/parsing_words/ask_question.rb +32 -0
- data/lib/tyrano_dsl/parsing_words/conditional_jump.rb +42 -0
- data/lib/tyrano_dsl/parsing_words/declare_background.rb +32 -0
- data/lib/tyrano_dsl/parsing_words/declare_character.rb +38 -0
- data/lib/tyrano_dsl/parsing_words/declare_label.rb +20 -0
- data/lib/tyrano_dsl/parsing_words/declare_variable.rb +30 -0
- data/lib/tyrano_dsl/parsing_words/display_text.rb +22 -0
- data/lib/tyrano_dsl/parsing_words/hide_character.rb +20 -0
- data/lib/tyrano_dsl/parsing_words/hide_message_window.rb +16 -0
- data/lib/tyrano_dsl/parsing_words/include_file.rb +26 -0
- data/lib/tyrano_dsl/parsing_words/jump.rb +22 -0
- data/lib/tyrano_dsl/parsing_words/parsing_words_module.rb +72 -0
- data/lib/tyrano_dsl/parsing_words/set_background.rb +21 -0
- data/lib/tyrano_dsl/parsing_words/set_character_stance.rb +23 -0
- data/lib/tyrano_dsl/parsing_words/set_title_screen_background.rb +25 -0
- data/lib/tyrano_dsl/parsing_words/show_character.rb +26 -0
- data/lib/tyrano_dsl/parsing_words/show_message_window.rb +16 -0
- data/lib/tyrano_dsl/parsing_words/start_scene.rb +30 -0
- data/lib/tyrano_dsl/parsing_words/update_variable.rb +37 -0
- data/lib/tyrano_dsl/tyrano_dsl.rb +3 -0
- data/lib/tyrano_dsl/tyrano_exception.rb +6 -0
- data/lib/tyrano_dsl/vocabulary.rb +84 -0
- data/lib/tyrano_dsl/writer.rb +114 -0
- data/lib/tyrano_dsl/writing_context.rb +123 -0
- data/lib/tyrano_dsl/writing_words/ask_question.rb +28 -0
- data/lib/tyrano_dsl/writing_words/conditional_jump.rb +37 -0
- data/lib/tyrano_dsl/writing_words/declare_background.rb +5 -0
- data/lib/tyrano_dsl/writing_words/declare_character.rb +5 -0
- data/lib/tyrano_dsl/writing_words/declare_label.rb +18 -0
- data/lib/tyrano_dsl/writing_words/declare_variable.rb +5 -0
- data/lib/tyrano_dsl/writing_words/display_text.rb +16 -0
- data/lib/tyrano_dsl/writing_words/hide_character.rb +13 -0
- data/lib/tyrano_dsl/writing_words/hide_message_window.rb +12 -0
- data/lib/tyrano_dsl/writing_words/include_file.rb +5 -0
- data/lib/tyrano_dsl/writing_words/jump.rb +18 -0
- data/lib/tyrano_dsl/writing_words/nop.rb +8 -0
- data/lib/tyrano_dsl/writing_words/set_background.rb +18 -0
- data/lib/tyrano_dsl/writing_words/set_character_stance.rb +20 -0
- data/lib/tyrano_dsl/writing_words/set_title_screen_background.rb +4 -0
- data/lib/tyrano_dsl/writing_words/show_character.rb +31 -0
- data/lib/tyrano_dsl/writing_words/show_message_window.rb +12 -0
- data/lib/tyrano_dsl/writing_words/start_scene.rb +9 -0
- data/lib/tyrano_dsl/writing_words/update_variable.rb +28 -0
- data/lib/tyrano_dsl/writing_words/writing_words_module.rb +43 -0
- data/tyrano_dsl.gemspec +37 -0
- metadata +210 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# A declared background
|
4
|
+
class TyranoDsl::Elements::Background
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :name
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :image_path
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :target_short_file_name
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :target_long_file_name
|
14
|
+
|
15
|
+
BACKGROUND_DIRECTORY = File.join('data', 'bgimage')
|
16
|
+
|
17
|
+
# @param [String] name
|
18
|
+
# @param [String] image_path
|
19
|
+
# @param [Integer] index
|
20
|
+
def initialize(name, image_path, index)
|
21
|
+
@name = name
|
22
|
+
@image_path = image_path
|
23
|
+
@index = index
|
24
|
+
@target_short_file_name = "#{index}#{File.extname(image_path)}"
|
25
|
+
@target_long_file_name = File.join(
|
26
|
+
BACKGROUND_DIRECTORY,
|
27
|
+
@target_short_file_name
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
require_relative 'stance'
|
3
|
+
|
4
|
+
|
5
|
+
# A declared character
|
6
|
+
class TyranoDsl::Elements::Character
|
7
|
+
|
8
|
+
CHARACTER_DIRECTORY = File.join('data', 'fgimage', 'chara')
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :name
|
12
|
+
# @return [Index]
|
13
|
+
attr_reader :index
|
14
|
+
# @return [Hash{String => TyranoDsl::Elements::Stance}]
|
15
|
+
attr_reader :stances
|
16
|
+
# @return [TyranoDsl::Elements::Stance]
|
17
|
+
attr_reader :default_stance
|
18
|
+
|
19
|
+
# @param [String] name
|
20
|
+
# @param [String] declared_stances
|
21
|
+
# @param [Integer] index
|
22
|
+
def initialize(name, declared_stances, index)
|
23
|
+
@name = name
|
24
|
+
@stances = stances
|
25
|
+
@index = index
|
26
|
+
@stances = {}
|
27
|
+
|
28
|
+
@stances_target_long_files_names = {}
|
29
|
+
@stance_target_short_file_names = {}
|
30
|
+
declared_stances.each_pair do |stance_name, stance_file|
|
31
|
+
short_file_name = File.join(index.to_s,
|
32
|
+
"#{@stances.length}#{File.extname(stance_file)}")
|
33
|
+
long_file_name = File.join(
|
34
|
+
CHARACTER_DIRECTORY, short_file_name)
|
35
|
+
stance = TyranoDsl::Elements::Stance.new(
|
36
|
+
stance_name,
|
37
|
+
stance_file,
|
38
|
+
short_file_name,
|
39
|
+
long_file_name)
|
40
|
+
@stances[stance_name] = stance
|
41
|
+
|
42
|
+
if stance_name == :default
|
43
|
+
@default_stance = stance
|
44
|
+
@default_stance_target_short_file_name = short_file_name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# A declared jump target
|
4
|
+
class TyranoDsl::Elements::JumpTarget
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :scene
|
8
|
+
# @return [TyranoDsl::Elements::Label,nil]
|
9
|
+
attr_reader :label
|
10
|
+
|
11
|
+
# @param [String] scene
|
12
|
+
# @param [TyranoDsl::Elements::Label,nil] label
|
13
|
+
def initialize(scene, label)
|
14
|
+
@scene = scene
|
15
|
+
@label = label
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# A label
|
4
|
+
|
5
|
+
class TyranoDsl::Elements::Label
|
6
|
+
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :name
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :target_name
|
11
|
+
|
12
|
+
# @param [String] name
|
13
|
+
# @param [String] target_name
|
14
|
+
def initialize(name, target_name)
|
15
|
+
@name = name
|
16
|
+
@target_name = target_name
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# A declared scene
|
4
|
+
class TyranoDsl::Elements::Scene
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :name
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :target_name
|
10
|
+
# @return [Array<String>]
|
11
|
+
attr_reader :labels
|
12
|
+
|
13
|
+
# @param [String] name
|
14
|
+
# @param [Integer] index
|
15
|
+
def initialize(name, index)
|
16
|
+
@name = name
|
17
|
+
@target_name = "scene#{index}"
|
18
|
+
@labels = []
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# A stance
|
4
|
+
class TyranoDsl::Elements::Stance
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :name
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :original_file_name
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :short_target_file_name
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :long_target_file_name
|
14
|
+
|
15
|
+
# @param [String] name
|
16
|
+
# @param [String] original_file_name
|
17
|
+
# @param [String] short_target_file_name
|
18
|
+
# @param [String] long_target_file_name
|
19
|
+
def initialize(name, original_file_name, short_target_file_name, long_target_file_name)
|
20
|
+
@name = name
|
21
|
+
@original_file_name = original_file_name
|
22
|
+
@short_target_file_name = short_target_file_name
|
23
|
+
@long_target_file_name = long_target_file_name
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# the title screen
|
4
|
+
class TyranoDsl::Elements::TitleScreen
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
attr_accessor :background
|
8
|
+
# @return [String]
|
9
|
+
attr_accessor :first_scene_name
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@background = nil
|
13
|
+
@first_scene_name = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
|
3
|
+
# A label
|
4
|
+
class TyranoDsl::Elements::Variable
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :name
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :target_name
|
10
|
+
# @return [Integer]
|
11
|
+
attr_reader :initial_value
|
12
|
+
|
13
|
+
# @param [String] name
|
14
|
+
# @param [String] target_name
|
15
|
+
# @param [String] initial_value
|
16
|
+
def initialize(name, target_name, initial_value)
|
17
|
+
@name = name
|
18
|
+
@target_name = target_name
|
19
|
+
@initial_value = initial_value
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative 'elements_module'
|
2
|
+
require_relative 'label'
|
3
|
+
require_relative 'title_screen'
|
4
|
+
require_relative 'variable'
|
5
|
+
|
6
|
+
# The game world
|
7
|
+
class TyranoDsl::Elements::World
|
8
|
+
|
9
|
+
# @return [Hash{String => TyranoDsl::Elements::Background}]
|
10
|
+
attr_reader :backgrounds
|
11
|
+
# @return [Hash{String => TyranoDsl::Elements::Character}]
|
12
|
+
attr_reader :characters
|
13
|
+
# @return [Array<TyranoDsl::Elements::JumpTarget>]
|
14
|
+
attr_reader :jump_targets
|
15
|
+
# @return [Hash{String => TyranoDsl::Elements::Label}]
|
16
|
+
attr_reader :labels
|
17
|
+
# @return [Hash{String => TyranoDsl::Elements::Scene}]
|
18
|
+
attr_reader :scenes
|
19
|
+
# @return [TyranoDsl::Elements::TitleScreen]
|
20
|
+
attr_reader :title_screen
|
21
|
+
# @return [Hash{String => TyranoDsl::Elements::Variable}]
|
22
|
+
attr_reader :variables
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@characters = {}
|
26
|
+
@backgrounds = {}
|
27
|
+
@labels = {}
|
28
|
+
@jump_targets = []
|
29
|
+
@scenes = {}
|
30
|
+
@variables = {}
|
31
|
+
@title_screen = TyranoDsl::Elements::TitleScreen.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [void]
|
35
|
+
# @raise [TyranoDsl::TyranoException]
|
36
|
+
def validate
|
37
|
+
@jump_targets.each do |jump_target|
|
38
|
+
scene = @scenes[jump_target.scene]
|
39
|
+
unless scene
|
40
|
+
raise TyranoDsl::TyranoException, "Unknown scene [#{jump_target.scene}] declared in label"
|
41
|
+
end
|
42
|
+
jump_target_label = jump_target.label
|
43
|
+
if jump_target_label && (!scene.labels.include?(jump_target_label.name))
|
44
|
+
raise TyranoDsl::TyranoException, "Unknown label [#{jump_target_label.name}] declared in label"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [String, nil] label_name
|
50
|
+
# @return [TyranoDsl::Elements::Label]
|
51
|
+
def label_value(label_name)
|
52
|
+
if @labels.key? label_name
|
53
|
+
@labels[label_name]
|
54
|
+
else
|
55
|
+
technical_name = label_name ? "label_#{@labels.length}" : nil
|
56
|
+
label = TyranoDsl::Elements::Label.new(label_name, technical_name)
|
57
|
+
@labels[label_name] = label
|
58
|
+
label
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../elements/background'
|
2
|
+
require_relative '../file_actions/clear_directory'
|
3
|
+
require_relative '../file_actions/file_copy'
|
4
|
+
require_relative 'elements_writers_module'
|
5
|
+
|
6
|
+
# Write a background
|
7
|
+
class TyranoDsl::ElementsWriters::BackgroundWriter
|
8
|
+
|
9
|
+
include TyranoDsl::ElementsWriters::ElementsWritersModule
|
10
|
+
|
11
|
+
# @return [Array]
|
12
|
+
def init_actions
|
13
|
+
[
|
14
|
+
TyranoDsl::FileActions::ClearDirectory.new(TyranoDsl::Elements::Background::BACKGROUND_DIRECTORY)
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [TyranoDsl::Elements::Background] background
|
19
|
+
# @return [Array]
|
20
|
+
def write(background)
|
21
|
+
log {"Writing background [#{background.name}]"}
|
22
|
+
[
|
23
|
+
TyranoDsl::FileActions::FileCopy.new(
|
24
|
+
background.image_path,
|
25
|
+
background.target_long_file_name
|
26
|
+
)
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative '../elements/character'
|
2
|
+
require_relative '../file_actions/clear_directory'
|
3
|
+
require_relative '../file_actions/file_copy'
|
4
|
+
require_relative 'elements_writers_module'
|
5
|
+
|
6
|
+
# Write a character
|
7
|
+
class TyranoDsl::ElementsWriters::CharacterWriter
|
8
|
+
|
9
|
+
include TyranoDsl::ElementsWriters::ElementsWritersModule
|
10
|
+
|
11
|
+
# @return [Array]
|
12
|
+
def init_actions
|
13
|
+
[
|
14
|
+
TyranoDsl::FileActions::ClearDirectory.new(TyranoDsl::Elements::Character::CHARACTER_DIRECTORY)
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [TyranoDsl::Elements::Character] character
|
19
|
+
# @return [Array]
|
20
|
+
def write(character)
|
21
|
+
log {"Writing character [#{character.name}]"}
|
22
|
+
result = []
|
23
|
+
character.stances.each_value do |stance|
|
24
|
+
result << TyranoDsl::FileActions::FileCopy.new(
|
25
|
+
stance.original_file_name,
|
26
|
+
stance.long_target_file_name
|
27
|
+
)
|
28
|
+
end
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../file_actions/create_file'
|
2
|
+
require_relative '../file_actions/json_patch'
|
3
|
+
require_relative 'elements_writers_module'
|
4
|
+
|
5
|
+
# Write things where all characters are implied
|
6
|
+
class TyranoDsl::ElementsWriters::CharactersWriter
|
7
|
+
|
8
|
+
include TyranoDsl::ElementsWriters::ElementsWritersModule
|
9
|
+
|
10
|
+
# @param [TyranoDsl::Elements::World] world
|
11
|
+
# @return [Array]
|
12
|
+
def write(world)
|
13
|
+
log {'Writing characters'}
|
14
|
+
chara_define_content = ''
|
15
|
+
world.characters.values.collect do |character|
|
16
|
+
chara_define_content << "[chara_new name=\"#{character.name}\" jname=\"#{character.name}\" storage=\"chara\/#{character.default_stance.short_target_file_name}\"]\n"
|
17
|
+
end
|
18
|
+
chara_define_content << "\n"
|
19
|
+
chara_define_content << "[iscript]\n"
|
20
|
+
world.variables.values.collect do |variable|
|
21
|
+
chara_define_content << "f['#{variable.target_name}']=#{variable.initial_value};\n"
|
22
|
+
end
|
23
|
+
chara_define_content << "[endscript]\n"
|
24
|
+
|
25
|
+
builder_config_content = {}
|
26
|
+
world.characters.values.each do |character|
|
27
|
+
builder_config_content[character.name] = character.index
|
28
|
+
end
|
29
|
+
|
30
|
+
[
|
31
|
+
TyranoDsl::FileActions::CreateFile.new(
|
32
|
+
File.join('data', 'scenario', 'system', 'chara_define.ks'),
|
33
|
+
chara_define_content
|
34
|
+
),
|
35
|
+
TyranoDsl::FileActions::JsonPatch.new(
|
36
|
+
'builder_config.json',
|
37
|
+
['map_chara'],
|
38
|
+
builder_config_content
|
39
|
+
)
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module TyranoDsl
|
4
|
+
module ElementsWriters
|
5
|
+
|
6
|
+
# Helpers to write writers
|
7
|
+
module ElementsWritersModule
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
# @param [Array<String>] pathes
|
12
|
+
def preload_text(pathes)
|
13
|
+
pathes.collect {|a| "[preload storage=\"./#{a}\"]\n"}.join + '[return]'
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
@logger ||= Logger.new(STDOUT)
|
18
|
+
end
|
19
|
+
|
20
|
+
def log
|
21
|
+
logger.info(self.class) {yield}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../file_actions/create_file'
|
2
|
+
require_relative 'elements_writers_module'
|
3
|
+
|
4
|
+
# Write a scene
|
5
|
+
class TyranoDsl::ElementsWriters::SceneWriter
|
6
|
+
|
7
|
+
include TyranoDsl::ElementsWriters::ElementsWritersModule
|
8
|
+
|
9
|
+
# @param [TyranoDsl::Elements::Scene] scene
|
10
|
+
# @param [Array<String>] scene_content
|
11
|
+
# @param [Array<String>] assets
|
12
|
+
# @return [Array]
|
13
|
+
def write(scene, scene_content, assets)
|
14
|
+
log {"Writing scene [#{scene.name}]"}
|
15
|
+
content_text_content = "[_tb_system_call storage=system/_#{scene.target_name}.ks]\n[cm]\n#{scene_content.join("\n")}\n"
|
16
|
+
preload_text_content = preload_text(assets.to_a)
|
17
|
+
[
|
18
|
+
TyranoDsl::FileActions::CreateFile.new(
|
19
|
+
File.join('data', 'scenario', "#{scene.target_name}.ks"),
|
20
|
+
content_text_content
|
21
|
+
),
|
22
|
+
TyranoDsl::FileActions::CreateFile.new(
|
23
|
+
File.join('data', 'scenario', 'system', "_#{scene.target_name}.ks"),
|
24
|
+
preload_text_content
|
25
|
+
)
|
26
|
+
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|