bcpm 0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +133 -0
- data/Manifest +23 -0
- data/README +292 -0
- data/Rakefile +23 -0
- data/bcpm.gemspec +35 -0
- data/bin/bcpm +5 -0
- data/lib/bcpm.rb +23 -0
- data/lib/bcpm/cleanup.rb +24 -0
- data/lib/bcpm/cli.rb +262 -0
- data/lib/bcpm/config.rb +100 -0
- data/lib/bcpm/dist.rb +133 -0
- data/lib/bcpm/duel.rb +153 -0
- data/lib/bcpm/git.rb +93 -0
- data/lib/bcpm/match.rb +275 -0
- data/lib/bcpm/player.rb +410 -0
- data/lib/bcpm/regen.rb +102 -0
- data/lib/bcpm/socket.rb +8 -0
- data/lib/bcpm/tests/assertion_error.rb +13 -0
- data/lib/bcpm/tests/assertions.rb +102 -0
- data/lib/bcpm/tests/case_base.rb +166 -0
- data/lib/bcpm/tests/environment.rb +256 -0
- data/lib/bcpm/tests/suite.rb +114 -0
- data/lib/bcpm/tests/test_match.rb +166 -0
- data/lib/bcpm/update.rb +38 -0
- metadata +129 -0
data/lib/bcpm/player.rb
ADDED
@@ -0,0 +1,410 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
# :nodoc: namespace
|
4
|
+
module Bcpm
|
5
|
+
|
6
|
+
# Manages player packages.
|
7
|
+
module Player
|
8
|
+
# Clones a player code from a repository, and sets it up for development on the local machine.
|
9
|
+
#
|
10
|
+
# Returns the player's name, or nil if something went wrong.
|
11
|
+
def self.install(repo_uri, repo_branch)
|
12
|
+
name = player_name repo_uri
|
13
|
+
local_path = File.join local_root, name
|
14
|
+
if File.exist?(local_path)
|
15
|
+
puts "Player already installed at #{local_path}!"
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
return nil unless Bcpm::Git.clone_repo(repo_uri, repo_branch, local_path)
|
19
|
+
|
20
|
+
unless source_path = package_path(local_path)
|
21
|
+
puts "Repository #{repo_uri} doesn't seem to contain a player!"
|
22
|
+
FileUtils.rm_rf local_path
|
23
|
+
return nil
|
24
|
+
end
|
25
|
+
|
26
|
+
Bcpm::Dist.add_player source_path
|
27
|
+
configure local_path
|
28
|
+
name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Downloads player code from a repository for one-time use, without linking it to the repository.
|
32
|
+
#
|
33
|
+
# Returns the path to the player on the local system, or nil if something went wrong.
|
34
|
+
def self.checkpoint(repo_uri, repo_branch, local_name)
|
35
|
+
old_name = player_name repo_uri
|
36
|
+
local_path = File.join local_root, local_name
|
37
|
+
if File.exist?(local_path)
|
38
|
+
puts "Player already installed at #{local_path}!"
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
if old_name == repo_uri
|
42
|
+
return nil unless Bcpm::Git.checkpoint_local_repo(File.join(local_root, old_name), local_path)
|
43
|
+
else
|
44
|
+
return nil unless Bcpm::Git.checkpoint_repo(repo_uri, repo_branch, local_path)
|
45
|
+
end
|
46
|
+
if local_name == old_name
|
47
|
+
source_path = package_path(local_path)
|
48
|
+
else
|
49
|
+
source_path = rename(local_path, old_name)
|
50
|
+
end
|
51
|
+
unless source_path
|
52
|
+
puts "Repository #{repo_uri} doesn't seem to contain a player!"
|
53
|
+
FileUtils.rm_rf local_path
|
54
|
+
return nil
|
55
|
+
end
|
56
|
+
|
57
|
+
Bcpm::Dist.add_player source_path
|
58
|
+
configure local_path
|
59
|
+
local_path
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a player from the built-in template.
|
63
|
+
#
|
64
|
+
# Returns the path to the player on the local system, or nil if something went wrong.
|
65
|
+
def self.create(local_name)
|
66
|
+
old_name = 'template'
|
67
|
+
local_path = File.join local_root, local_name
|
68
|
+
if File.exist?(local_path)
|
69
|
+
puts "Player already installed at #{local_path}!"
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
write_template local_path
|
73
|
+
unless source_path = rename(local_path, old_name)
|
74
|
+
puts "Repository #{repo_uri} doesn't seem to contain a player!"
|
75
|
+
FileUtils.rm_rf local_path
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
|
79
|
+
Bcpm::Dist.add_player source_path
|
80
|
+
configure local_path
|
81
|
+
local_path
|
82
|
+
end
|
83
|
+
|
84
|
+
# Undoes the effects of an install or checkpoint call.
|
85
|
+
#
|
86
|
+
# Returns true for success, or false if something went wrong.
|
87
|
+
def self.uninstall(local_name)
|
88
|
+
local_path = File.join local_root, local_name
|
89
|
+
source_path = package_path local_path
|
90
|
+
Bcpm::Dist.remove_player source_path if source_path
|
91
|
+
return false unless File.exist? local_path
|
92
|
+
FileUtils.rm_rf local_path
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Re-configures a player's source code project.
|
97
|
+
def self.reconfigure(local_name)
|
98
|
+
local_path = File.join local_root, local_name
|
99
|
+
unless source_path = package_path(local_path)
|
100
|
+
puts "Directory #{local_path} doesn't seem to contain a player!"
|
101
|
+
FileUtils.rm_rf local_path
|
102
|
+
return nil
|
103
|
+
end
|
104
|
+
Bcpm::Dist.add_player source_path
|
105
|
+
configure local_path
|
106
|
+
end
|
107
|
+
|
108
|
+
# Runs the player's test suite.
|
109
|
+
def self.run_suite(local_name)
|
110
|
+
local_path = File.join local_root, local_name
|
111
|
+
unless has_suite? local_path
|
112
|
+
puts "No test suite found at #{local_path}!"
|
113
|
+
return false
|
114
|
+
end
|
115
|
+
new_suite(local_path).run false
|
116
|
+
end
|
117
|
+
|
118
|
+
# Runs a case in the test suite against a player codebase.
|
119
|
+
def self.run_case(case_name, live, local_name, branch = 'master')
|
120
|
+
local_path = File.join local_root, local_name
|
121
|
+
unless has_suite? local_path
|
122
|
+
puts "No test suite found at #{local_path}!"
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
|
126
|
+
case_file = case_name + '.rb'
|
127
|
+
files = suite_files(local_path).select { |file| File.basename(file) == case_file }
|
128
|
+
unless files.length == 1
|
129
|
+
puts "Ambiguous case name! It matched #{files.count} cases.\n#{files.join("\n")}\n"
|
130
|
+
return false
|
131
|
+
end
|
132
|
+
suite = Bcpm::Tests::Suite.new(local_path)
|
133
|
+
suite.add_cases files
|
134
|
+
suite.run live
|
135
|
+
end
|
136
|
+
|
137
|
+
# Creates a Suite instance for running all the tests.
|
138
|
+
def self.new_suite(local_path)
|
139
|
+
suite = Bcpm::Tests::Suite.new local_path
|
140
|
+
suite.add_cases suite_files(local_path)
|
141
|
+
suite
|
142
|
+
end
|
143
|
+
|
144
|
+
# All the test cases in the suite of the given player.
|
145
|
+
def self.suite_files(local_path)
|
146
|
+
Dir.glob File.join(local_path, 'suite', '**', '*.rb')
|
147
|
+
end
|
148
|
+
|
149
|
+
# True if a battlecode distribution is installed on the local machine.
|
150
|
+
def self.has_suite?(local_path)
|
151
|
+
File.exist? File.join(local_path, 'suite')
|
152
|
+
end
|
153
|
+
|
154
|
+
# Configures a player's source code project.
|
155
|
+
def self.configure(local_path)
|
156
|
+
File.open File.join(local_path, '.project'), 'wb' do |f|
|
157
|
+
f.write eclipse_project(local_path)
|
158
|
+
end
|
159
|
+
|
160
|
+
File.open File.join(local_path, '.classpath'), 'wb' do |f|
|
161
|
+
f.write eclipse_classpath(local_path)
|
162
|
+
end
|
163
|
+
|
164
|
+
File.open File.join(local_path, 'build.xml'), 'wb' do |f|
|
165
|
+
f.write ant_config('bc.conf')
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# The directory containing all players code.
|
170
|
+
def self.local_root
|
171
|
+
Bcpm::Config[:player_root] ||= default_local_root
|
172
|
+
end
|
173
|
+
|
174
|
+
# The directory containing all players code.
|
175
|
+
def self.default_local_root
|
176
|
+
path = Dir.pwd
|
177
|
+
unless File.exist?(File.join(path, '.metadata'))
|
178
|
+
puts "Please chdir into your Eclipse workspace!"
|
179
|
+
exit 1
|
180
|
+
end
|
181
|
+
path
|
182
|
+
end
|
183
|
+
|
184
|
+
# All installed players.
|
185
|
+
#
|
186
|
+
# This might contain false positives.
|
187
|
+
def self.list
|
188
|
+
Dir.glob(File.join(local_root, '*', '.project')).map do |project_file|
|
189
|
+
File.basename File.dirname(project_file)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# All installed players who are properly hooked into the distribution.
|
194
|
+
#
|
195
|
+
# These players can be used into tests and simulations.
|
196
|
+
def self.list_active
|
197
|
+
list.select { |player_name| wired? player_name }
|
198
|
+
end
|
199
|
+
|
200
|
+
# True if the given player name is installed and wired.
|
201
|
+
def self.wired?(player_name)
|
202
|
+
local_path = File.join local_root, player_name
|
203
|
+
source_path = package_path local_path, nil, true
|
204
|
+
(source_path && Bcpm::Dist.contains_player?(source_path)) ? true : false
|
205
|
+
end
|
206
|
+
|
207
|
+
# Cleans up all the installed players.
|
208
|
+
def self.uninstall_all
|
209
|
+
list_good.each { |player_name| uninstall player_name }
|
210
|
+
end
|
211
|
+
|
212
|
+
# Extracts the player name out of the git repository URI for the player's code.
|
213
|
+
def self.player_name(repo_uri)
|
214
|
+
name = File.basename(repo_uri)
|
215
|
+
name = name[0...-4] if name[-4, 4] == '.git'
|
216
|
+
name
|
217
|
+
end
|
218
|
+
|
219
|
+
# Renames a player to match its path on the local system.
|
220
|
+
#
|
221
|
+
# Returns the path to the player's source package.
|
222
|
+
def self.rename(local_path, old_name)
|
223
|
+
new_name = File.basename local_path
|
224
|
+
return nil unless old_source_dir = package_path(local_path, old_name)
|
225
|
+
new_source_dir = File.join File.dirname(old_source_dir), new_name
|
226
|
+
FileUtils.mv old_source_dir, new_source_dir
|
227
|
+
|
228
|
+
Dir.glob(File.join(new_source_dir, '**', '*.java')).each do |file|
|
229
|
+
contents = File.read file
|
230
|
+
contents.gsub! /(^|[^A-Za-z0-9_.])#{old_name}([^A-Za-z0-9_]|$)/, "\\1#{new_name}\\2"
|
231
|
+
File.open(file, 'wb') { |f| f.write contents }
|
232
|
+
end
|
233
|
+
new_source_dir
|
234
|
+
end
|
235
|
+
|
236
|
+
# Extracts the path to a player's source package given their repository.
|
237
|
+
#
|
238
|
+
# Args:
|
239
|
+
# local_path:: path to the player's git repository on the local machine
|
240
|
+
# name_override:: (optional) supplies the player name; if not set, the name is extracted from
|
241
|
+
# the path, by convention
|
242
|
+
# silent:: if set, won't output errors that help the user debug the problem
|
243
|
+
def self.package_path(local_path, name_override = nil, silent = false)
|
244
|
+
# All the packages should be in the 'src' directory.
|
245
|
+
source_dir = File.join local_path, 'src'
|
246
|
+
unless File.exist? source_dir
|
247
|
+
puts "Missing src directory" unless silent
|
248
|
+
return nil
|
249
|
+
end
|
250
|
+
|
251
|
+
# Ignore maintainance files/folder such as .gitignore / .svn.
|
252
|
+
package_dirs = Dir.glob(File.join(source_dir, '*')).
|
253
|
+
reject { |path| File.basename(path)[0, 1] == '.' }
|
254
|
+
unless package_dirs.length == 1
|
255
|
+
puts "src directory doesn't contain exactly one package directory!" unless silent
|
256
|
+
return nil
|
257
|
+
end
|
258
|
+
|
259
|
+
path = package_dirs.first
|
260
|
+
unless (name_override || File.basename(local_path)) == File.basename(path)
|
261
|
+
puts "The package in the src directory doesn't match the player name!" unless silent
|
262
|
+
return nil
|
263
|
+
end
|
264
|
+
path
|
265
|
+
end
|
266
|
+
|
267
|
+
# The contents of an Ant configuration file (build.xml) pointing to a simulator config file.
|
268
|
+
def self.ant_config(simulator_config)
|
269
|
+
contents = File.read Bcpm::Dist.ant_file
|
270
|
+
# Point to the distribution instead of current root.
|
271
|
+
contents.gsub! 'basedir="."', 'basedir="' + Bcpm::Dist.dist_path + '"'
|
272
|
+
contents.gsub! '<property name="path.base" location="."',
|
273
|
+
'<property name="path.base" location="' + Bcpm::Dist.dist_path + '"'
|
274
|
+
# Set VM memory.
|
275
|
+
contents.gsub! /\<jvmarg\s+value\=\"\-Xmx.*\"/,
|
276
|
+
"<jvmarg value=\"-Xmx#{Bcpm::Match::vm_ram}m\""
|
277
|
+
# Replace hardcoded bc.conf reference.
|
278
|
+
contents.gsub! 'bc.conf', simulator_config
|
279
|
+
contents
|
280
|
+
end
|
281
|
+
|
282
|
+
# The contents of an Eclipse .classpath for a player project.
|
283
|
+
def self.eclipse_classpath(local_path)
|
284
|
+
jar_path = File.join Bcpm::Dist.dist_path, 'lib', 'battlecode-server.jar'
|
285
|
+
|
286
|
+
<<END_CONFIG
|
287
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
288
|
+
<classpath>
|
289
|
+
<classpathentry kind="src" path="src"/>
|
290
|
+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
291
|
+
<classpathentry kind="lib" path="#{jar_path}"/>
|
292
|
+
<classpathentry kind="output" path="bin"/>
|
293
|
+
</classpath>
|
294
|
+
END_CONFIG
|
295
|
+
end
|
296
|
+
|
297
|
+
# The contents of an Eclipse .project file for a player project.
|
298
|
+
def self.eclipse_project(local_path)
|
299
|
+
project_name = File.basename local_path
|
300
|
+
|
301
|
+
<<END_CONFIG
|
302
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
303
|
+
<projectDescription>
|
304
|
+
<name>#{project_name}</name>
|
305
|
+
<comment></comment>
|
306
|
+
<projects>
|
307
|
+
</projects>
|
308
|
+
<buildSpec>
|
309
|
+
<buildCommand>
|
310
|
+
<name>org.eclipse.jdt.core.javabuilder</name>
|
311
|
+
<arguments>
|
312
|
+
</arguments>
|
313
|
+
</buildCommand>
|
314
|
+
</buildSpec>
|
315
|
+
<natures>
|
316
|
+
<nature>org.eclipse.jdt.core.javanature</nature>
|
317
|
+
</natures>
|
318
|
+
</projectDescription>
|
319
|
+
END_CONFIG
|
320
|
+
end
|
321
|
+
|
322
|
+
# Writes the built-in player template.
|
323
|
+
def self.write_template(local_path)
|
324
|
+
src_path = File.join local_path, 'src', 'template'
|
325
|
+
FileUtils.mkdir_p src_path
|
326
|
+
File.open File.join(src_path, 'RobotPlayer.java'), 'wb' do |f|
|
327
|
+
f.write <<END_SOURCE
|
328
|
+
package template;
|
329
|
+
|
330
|
+
import battlecode.common.RobotController;
|
331
|
+
|
332
|
+
public class RobotPlayer implements Runnable {
|
333
|
+
public static RobotController rc;
|
334
|
+
|
335
|
+
public RobotPlayer(RobotController controller) {
|
336
|
+
rc = controller;
|
337
|
+
}
|
338
|
+
|
339
|
+
public void run() {
|
340
|
+
while (true) {
|
341
|
+
rc.yield();
|
342
|
+
}
|
343
|
+
}
|
344
|
+
}
|
345
|
+
END_SOURCE
|
346
|
+
end
|
347
|
+
|
348
|
+
src_path = File.join local_path, 'src', 'template', 'test', 'players'
|
349
|
+
FileUtils.mkdir_p src_path
|
350
|
+
File.open File.join(src_path, 'RobotPlayer.java'), 'wb' do |f|
|
351
|
+
f.write <<END_SOURCE
|
352
|
+
package template.test.players;
|
353
|
+
|
354
|
+
import battlecode.common.RobotController;
|
355
|
+
|
356
|
+
public class RobotPlayer implements Runnable {
|
357
|
+
public static RobotController rc;
|
358
|
+
|
359
|
+
public RobotPlayer(RobotController controller) {
|
360
|
+
rc = controller;
|
361
|
+
}
|
362
|
+
|
363
|
+
public void run() {
|
364
|
+
while (true) {
|
365
|
+
rc.yield();
|
366
|
+
}
|
367
|
+
}
|
368
|
+
}
|
369
|
+
END_SOURCE
|
370
|
+
end
|
371
|
+
|
372
|
+
suite_path = File.join local_path, 'suite'
|
373
|
+
FileUtils.mkdir_p suite_path
|
374
|
+
File.open File.join(suite_path, 'win_vs_yield.rb'), 'wb' do |f|
|
375
|
+
f.write <<END_SOURCE
|
376
|
+
vs 'yield'
|
377
|
+
suite_map '#{Bcpm::Dist.maps.first}'
|
378
|
+
replace_class 'RobotPlayer', 'test.players.RobotPlayer'
|
379
|
+
|
380
|
+
match do
|
381
|
+
it 'must win in any way' do
|
382
|
+
should_win
|
383
|
+
end
|
384
|
+
end
|
385
|
+
END_SOURCE
|
386
|
+
end
|
387
|
+
|
388
|
+
maps_path = File.join suite_path, 'maps'
|
389
|
+
FileUtils.mkdir_p maps_path
|
390
|
+
Bcpm::Dist.copy_map Bcpm::Dist.maps.first, maps_path
|
391
|
+
|
392
|
+
File.open File.join(local_path, '.gitignore'), 'wb' do |f|
|
393
|
+
f.write <<END_SOURCE
|
394
|
+
# Auto-generated by bcpm.
|
395
|
+
build.xml
|
396
|
+
.classpath
|
397
|
+
.project
|
398
|
+
|
399
|
+
# Build output.
|
400
|
+
bin
|
401
|
+
|
402
|
+
# Temporary files.
|
403
|
+
~*
|
404
|
+
.DS_Store
|
405
|
+
END_SOURCE
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end # module Bcpm::Player
|
409
|
+
|
410
|
+
end # namespace Bcpm
|
data/lib/bcpm/regen.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Bcpm
|
3
|
+
|
4
|
+
# Source code auto-generation capabilities.
|
5
|
+
module Regen
|
6
|
+
# Re-generates automatically generated source code files.
|
7
|
+
def self.run(file_names)
|
8
|
+
source_lines = {}
|
9
|
+
vars = {}
|
10
|
+
|
11
|
+
# Read in source blocks.
|
12
|
+
file_names.each do |file_name|
|
13
|
+
lines = File.open(file_name, 'rb') { |f| f.read.split "\n" }
|
14
|
+
|
15
|
+
current_block = nil
|
16
|
+
lines.each do |line|
|
17
|
+
if current_block
|
18
|
+
if /^\s+\/\/\$\s+\-gen\:source(\s.*)?$/ =~ line
|
19
|
+
current_block = nil
|
20
|
+
else
|
21
|
+
source_lines[current_block] << line
|
22
|
+
end
|
23
|
+
else
|
24
|
+
block_match = /^\s+\/\/\$\s+\+gen\:source\s+(\S+)\s+(.*)$/.match line
|
25
|
+
if block_match
|
26
|
+
current_block = block_match[1]
|
27
|
+
if source_lines[current_block]
|
28
|
+
print "Duplicate source block #{current_block}\n"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
source_lines[current_block] ||= []
|
32
|
+
vars[current_block] = block_match[2].scan /\S+/
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
if current_block
|
37
|
+
print "Un-closed source block #{current_block}\n"
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Replace target blocks.
|
43
|
+
file_names.each do |file_name|
|
44
|
+
lines = File.open(file_name, 'rb') { |f| f.read.split "\n" }
|
45
|
+
output_lines = []
|
46
|
+
|
47
|
+
current_block = nil
|
48
|
+
disabled = false
|
49
|
+
lines.each do |line|
|
50
|
+
if current_block
|
51
|
+
if /^\s+\/\/\$\s+\-gen\:target(\s.*)?$/ =~ line
|
52
|
+
output_lines << line
|
53
|
+
current_block = nil
|
54
|
+
end
|
55
|
+
else
|
56
|
+
block_match = /^\s+\/\/\$\s+\+gen\:target\s+(\S+)\s+(.*)$/.match line
|
57
|
+
output_lines << line
|
58
|
+
|
59
|
+
if block_match
|
60
|
+
current_block = block_match[1]
|
61
|
+
source_vars = vars[current_block]
|
62
|
+
if !source_vars
|
63
|
+
print "Missing source block #{current_block}\n"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
target_vars = block_match[2].scan /\S+/
|
67
|
+
if target_vars.length != source_vars.length
|
68
|
+
print "Source/target variable mismatch.\n"
|
69
|
+
print "Source: #{vars.join(' ')}\nTarget: #{vars.join(' ')}\n"
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
source_target = Hash[source_vars.zip(target_vars)]
|
74
|
+
regexp = Regexp.new source_vars.map { |var| "(#{var})" }.join('|')
|
75
|
+
new_lines = source_lines[current_block].map &:dup
|
76
|
+
disabled = false
|
77
|
+
new_lines.each do |line|
|
78
|
+
if disabled
|
79
|
+
disabled = false if /^\s+\/\/\$\s+\-gen\:off(\s.*)?$/ =~ line
|
80
|
+
else
|
81
|
+
if /^\s+\/\/\$\s+\+gen\:off(\s.*)?$/ =~ line
|
82
|
+
disabled = true
|
83
|
+
else
|
84
|
+
line.gsub!(regexp) { |match| source_target[match] }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
output_lines.concat new_lines
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if current_block
|
93
|
+
print "Un-closed target block #{current_block}\n"
|
94
|
+
exit
|
95
|
+
end
|
96
|
+
|
97
|
+
File.open(file_name, 'wb') { |f| f.write output_lines.join("\n") }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end # module Bcpm::Regen
|
101
|
+
|
102
|
+
end # namespace Bcpm
|