-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathhaste.pl
executable file
·498 lines (425 loc) · 18.8 KB
/
haste.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
#!/usr/bin/perl
use strict;
use warnings;
use feature ':5.10';
use Carp;
use IO::Handle;
use JSON::PP;
use List::Util qw(sum max min);
use Data::Dumper;
use Getopt::Long;
use Client;
use Tools qw( :all );
autoflush STDOUT 1;
autoflush STDERR 1;
my @owe = qw( ore water energy ); # We'll repeat this a lot
our %opts;
our $usage = qq{
Usage: $0 [options]
This script attempts to maintain waste levels within your specified parameters,
sustaining a minimum amount of waste for your waste consuming planets and
keeping waste below a maximum percentage of your storage. It adapts to the
capacity and production levels for each planet.
When waste production is negative, it will dump resources in a balanced manner
to bring down the most abundant resource down first to generate waste. For
resources that have subtypes (food and ore), this balancing extends to the
subtypes.
When waste production is positive, it will attempt to bring the amount of
stored waste below the maximum percentage of storage by using scows to dump
waste in the local star and then by recycling waste into usable resources.
Recycling is done in batches to take an --iteration's worth of time, if run
in continuous mode, or 10 minutes' worth of time if it is a single run.
It reports its actions so you can always see what it has done.
Options:
--config <empire.json> - JSON configuration file for empire login
--dry-run - Do not take any real action. Useful for testing
options to see how different combinations will
affect behavior
--help - Prints this help message
--quiet - Suppresses most output (TODO)
--verbose - Provides additional information about how decisions
are made
--planet <p1[,p2,...]> - List of planets to run against
--interval <minutes> - Minutes to sleep between runs. Also controls recycle
center batches
--batch-time <minutes> - Minutes to make recycling batches fill. Falls back to
--interval or defaults to 10 minutes
--recycle-balanced - Forces haste to recycle balanced amounts between all
resources
--recycle-by-amount - Recycling focuses on the resource with the smallest
amount stored at the time of recycling.
--recycle-by-rate - Recycling focuses on the resource with the slowest
production rate
--keep-hours <hours> - Minimum number of hours of waste to keep. This uses
the planet's waste per hour metric to determine the
minimum amount of waste to keep.
--keep-units <units> - Minimum amount of waste to keep.
--min-percent <.00> - Minimum percentage of waste storage to keep filled.
This uses the planet's waste storage capacity to
calculate the minimum amount of waste to keep.
--max-percent <.00> - Maximum percentage of waste storage to keep filled.
This uses the planet's waste storage capacity to
calculate the maximum amount of waste to keep.
};
my $client;
Main( @ARGV );
exit(0);
sub Main {
local @ARGV = @_;
GetOptions(\%opts,
'config|c=s',
'debug|d!',
'dry-run|n',
'help|h!',
'quiet|q!',
'verbose|v!',
'planet|p=s@',
'interval|i|s|sleep=i',
'batch-size|batch|b=i',
'keep-hours|kh=f',
'keep-units|ku=f',
'min-percent|min=f',
'max-percent|max=f',
'recycle-balanced|rb',
'recycle-by-amount|ra',
'recycle-by-rate|rr',
) or usage();
usage() if $opts{help};
# Ensure we have some kind of threshold
unless ( grep { $opts{$_} } qw( keep-hours keep-kilos min-percent max-percent ) ) {
usage();
}
# check for list of planets to work on
my %do_planets;
if ($opts{planet}) {
%do_planets = map { normalize_planet($_) => 1 } @{$opts{planet}};
}
# instantiate the client and get initial status
my $config = $opts{config} || shift @ARGV || 'config.json' ;
$client = Client->new(config => $config);
my $empire = $client->empire_status();
# Take $empire->{planets} and turn it inside-out, but cross-reference %do_planets to
# 1) get a hash of only the planets we care about; or
# 2) get a hash of *all* planets
my $grep = ( keys %do_planets )
? sub { exists $do_planets{ normalize_planet( $empire->{planets}{$_} ) } }
: sub { 1 }
;
my %planets =
map { $empire->{planets}{$_} => $_ }
grep { $grep->($_) }
keys %{ $empire->{planets} }
;
# If we have a list of planets to work on, we only want to do those planets; otherwise, we do all planets.
%do_planets = map { $empire->{planets}{$_}, $_ } keys %{$empire->{planets}} unless ( keys %do_planets );
do {
for my $planet_name ( keys %do_planets ) {
output("Checking waste stores on $planet_name");
my $s = $client->body_status($planets{$planet_name});
# use opts to determine the largest minimum waste / smallest maximum waste threshholds
my $minimum_waste = max(
( $opts{'keep-hours'} ? abs( $s->{waste_hour} * $opts{'keep-hours'} ) : () ),
( $opts{'keep-units'} ? $opts{'keep-units'} : () ),
( $opts{'min-percent'} ? int( $s->{waste_capacity} * $opts{'min-percent'} ) : () ),
0
);
my $maximum_waste =
( $opts{'max-percent'} )
? int( $s->{waste_capacity} * $opts{'max-percent'} )
: $s->{waste_capacity}
;
verbose("keeping waste between $minimum_waste and $maximum_waste units");
if ($s->{waste_hour} < 0) { # we're burning waste
output("Waste rate is negative ($s->{waste_hour})");
my $hours_left = sprintf('%0.2f', -1 * $s->{waste_stored} / $s->{waste_hour});
output("We have $hours_left hours ($s->{waste_stored} units) of waste stored");
}
elsif ($s->{waste_hour} > 0) { # we are making waste already
output("Waste rate is positive ($s->{waste_hour})");
my $hours_full = sprintf('%0.2f',
( $s->{waste_capacity} - $s->{waste_stored} ) / $s->{waste_hour}
);
output("$hours_full hours until waste is full; currently at $s->{waste_stored} units");
}
else {
output("You have attained waste zen.");
}
if ( $s->{waste_stored} < $minimum_waste ) {
verbose("We have less than our threshold of $minimum_waste units of waste");
make_waste( $s, $minimum_waste - $s->{waste_stored} );
}
elsif ( $s->{waste_stored} > $maximum_waste ) {
verbose( "More than $maximum_waste stored; disposing of some" );
waste_disposal( $s, $s->{waste_stored} - $maximum_waste );
}
else {
verbose("Stored waste is currently 'in-the-zone'. Do nothing.");
}
output("$planet_name ... done");
# grab the buildings on this planet
my $building_status = $client->body_buildings($planets{$planet_name});
my $buildings = $building_status->{buildings};
}
output(
scalar(localtime(time)),
' All planets are done.',
($opts{interval}?" Sleeping $opts{interval} minutes":''),
);
} while ( $opts{interval} && sleep( $opts{interval} * 60 ) );
return;
}
sub make_waste {
my ($s, $limit) = @_;
verbose("Need to create $limit units of waste");
verbose("Gathering storage facility information");
# get the storage facilities on this planet
my $body_buildings = $client->body_buildings($s->{id})->{'buildings'};
my %seen;
my %storage_facilities =
map { $body_buildings->{$_}{type} => +{ %{ $body_buildings->{$_} }, id => $_ } }
grep {
my ($type) = map { lc $_ } ($body_buildings->{$_}{name} =~ /^(\w+) (?:Storage|Reserve)/);
( $type && !$seen{$type} )
? ($seen{$type},$body_buildings->{$_}{type}) = (1,$type) # yes, this is sneaking a new key into items we want
: 0
}
keys %{$body_buildings};
unless ( keys %storage_facilities ) {
output( 'No storage facilities found; cannot produce waste!' );
return;
}
output("Gathering resource information");
# only consider resources that we can dump
my %resources = map { $_ => $s->{"${_}_stored"} } keys %seen;
debug( 'current resources: ', Dumper( \%resources ) );
my %resource_dump = proportionalize( $limit, \%resources );
debug("proportionalized dump: ",Dumper( \%resource_dump ) );
debug('storage facilities: ',Dumper(\%storage_facilities));
dump_resource( $storage_facilities{$_}, $_, $resource_dump{ $_ } ) for ( keys %resource_dump );
$client->cache_invalidate( type => 'body_status', id => $s->{id} );
return;
}
sub dump_resource {
my ($building, $type, $amount) = @_;
debug( 'building: ',Dumper( $building ) );
output("Dump $amount units of $type");
return unless $amount;
given ($type) {
when ('energy') {
$client->call('energyreserve','dump',$building->{id}, $amount) unless $opts{'dry-run'};
}
when ('water') {
$client->call('waterstorage','dump',$building->{id}, $amount) unless $opts{'dry-run'};
}
when ('food') {
my $food_stored = $client->call('foodreserve','view',$building->{id})->{'food_stored'};
my %food_types = proportionalize($amount, $food_stored);
while ( my ($specifically, $amount) = each %food_types ) {
next unless $amount;
output("Dumping $amount units of $specifically");
$client->call('foodreserve','dump',$building->{id}, $specifically, $amount)
unless $opts{'dry-run'};
}
}
when ('ore') {
my $ore_stored = $client->call('orestorage','view',$building->{id})->{'ore_stored'};
my %ore_types = proportionalize($amount, $ore_stored);
while ( my ($specifically, $amount) = each %ore_types ) {
next unless $amount;
output("Dumping $amount units of $specifically");
$client->call('orestorage','dump',$building->{id}, $specifically, $amount)
unless $opts{'dry-run'};
}
}
default {
output("I don't know how to dump $type\n");
}
}
}
sub waste_disposal {
my ($s, $amount) = @_;
my $recycled_amount = 0;
my $recycled = recycle( $s, $amount );
if (keys %$recycled) {
$recycled_amount = sum( values %$recycled );
output(
sprintf(
"Recycled %d units of waste into %s",
$recycled_amount,
join('; ', map { "$_: $recycled->{$_}" } keys %$recycled ),
)
);
}
else {
output("Could not recycle waste into resources");
}
return if ($recycled_amount >= $amount);
my $dumped = scow_dump( $s, ($amount - $recycled_amount) );
output("Shipped off $dumped units of waste via scow");
$client->cache_invalidate( type => 'body_status', id => $s->{id} );
}
sub proportionalize {
my ($limit, $resources) = @_;
verbose("Proportionalizing $limit units between @{[ scalar keys %$resources ]} resource types");
# Let's see what we're looking at...sorted by amount, descending
my @types = sort { $resources->{$b} <=> $resources->{$a} } keys %$resources;
# we still have to dump $limit's worth
my $remainder = $limit;
my %dump_amount;
for my $ii ( 0 .. $#types - 1) {
# Initialize the amount we want to dump
$dump_amount{ $types[$ii] } = 0;
# How much do we have between our current type and the next most plentiful type?
my $diff = $resources->{ $types[$ii] } - $resources->{ $types[$ii + 1] };
verbose( "diff between $types[$ii] and $types[$ii + 1] is $diff" );
# jump to the next resource if difference is 0
next unless $diff;
# If that difference is greater than what we still need to dump, only dump
# what we still need to in order to hit the limit
my $amount = ( $diff > $remainder ) ? $remainder : $diff;
verbose( "split $amount between @{[ scalar keys %dump_amount ]} resources" );
# divide that amount by the total number of things that we have queued to dump
$amount = int( ( $amount ) / scalar keys %dump_amount )||1;
$dump_amount{ $_ } += $amount for keys %dump_amount;
debug( "current split: ",Dumper(\%dump_amount) );
if ( (my $dumped = sum( values %dump_amount )) >= $limit ) { # we've dumped enough waste
verbose("accumulated dump amount: $dumped; done");
# we're done
last;
}
else {
# subtract out the dumped amount from the remainder
$remainder = $limit - $dumped;
verbose("accumulated dump amount: $dumped; $remainder units remaining to be dumped");
}
}
if ( keys %dump_amount && 0 == sum( values %dump_amount ) ) {
verbose( "Looks like you have equal amounts of all resources; evenly splitting total dump amount" );
my $amount = int( ( $limit ) / scalar keys %dump_amount )||1;
$dump_amount{ $_ } += $amount for keys %dump_amount;
}
return %dump_amount;
}
sub scow_dump {
my ($s, $amount) = @_;
my $dumped = 0;
my $target = { star_id => $s->{star_id} };
my $ships = $client->call(spaceport => get_ships_for => $s->{id}, $target);
if ($opts{verbose}) {
verbose("Ship count: ".scalar(@{$ships->{available}}));
for my $ship (@{$ships->{available}}) {
verbose("$ship->{id}: $ship->{type} $ship->{task} $ship->{hold_size}");
}
}
for my $scow (
sort { $b->{hold_size} <=> $a->{hold_size} }
grep { $_->{type} =~ /scow/ && $_->{task} eq "Docked" }
@{$ships->{available}}
) {
my $result = eval {
$client->call(spaceport => send_ship => $scow->{id}, $target) unless $opts{'dry-run'};
};
if ( $@ ) {
verbose("Sending scow $scow->{id} failed: $!");
next;
}
output("Sent scow '$scow->{name}' ($scow->{hold_size} waste) to $s->{star_name}");
$dumped += $scow->{hold_size};
verbose("dupmed $dumped of $amount so far");
last if ($dumped >= $amount);
}
if ( $dumped == 0 ) {
output( "Nothing dumped via scows. Do you have any scows available?" );
}
else {
output( "Dumped a total of $dumped via scows" );
}
return $dumped;
}
sub recycle {
my ($s, $amount) = @_;
my %recycled;
output( "Looking to recycle $amount of waste into new resources" );
my $buildings = $client->body_buildings($s->{id})->{'buildings'};
my @buildings = map { +{ %{ $buildings->{$_} }, id => $_ } } keys(%$buildings);
my @centers =
sort { $b->{level} <=> $a->{level} }
grep { $_->{name} =~ /^Waste (?:Recycling Center|Exchanger)$/ }
@buildings;
verbose( "We have @{[ scalar @centers || 0 ]} recycling buildings on this planet." );
unless ( scalar @centers ) {
output( "No recycling buildings; nothing to do." );
return \%recycled;
}
my $left_to_recycle = $amount;
for my $center (@centers) {
if ( $left_to_recycle <= 0 ) {
verbose( "Already recycled $amount; nothing left to recycle." );
last;
}
if ( $center->{work} ) {
verbose( "$center->{name} $center->{id} is busy; skipping\n" );
next;
}
my $view = $client->building_view($center->{url}, $center->{id});
my $recycle_capacity = int List::Util::min(
( ( ( ($opts{'batch-size'}||$opts{interval}||10) * 60 ) - 30 )
/ $view->{recycle}{seconds_per_resource} ),
$left_to_recycle,
$client->body_status($s->{id})->{'waste_stored'}
);
verbose("recycling $recycle_capacity units of waste into new resources at center $center->{id}");
# pull our resource info out
my $resources;
for my $res ( @owe ) {
for my $stat ( qw( hour stored capacity ) ) {
$resources->{"${res}_${stat}"} = $s->{"${res}_${stat}"};
}
}
# determine what we want to make (hint: don't make it if it would excede capacity)
my %producing = map { $_ => ( $resources->{ "${_}_capacity" } > $resources->{ "${_}_stored" } + 1 ) } @owe;
my $production_count = sum( values %producing );
unless ( $production_count ) {
verbose("All storage is full. Falling back to balanced recycling.");
$production_count = scalar @owe;
@producing{ @owe } = (1) x $production_count;
}
#initialize our hash
my %recycle = map { $_ => 0 } @owe;
if ( $opts{'recycle-by-rate'} ) {
my ($focus) = sort { $resources->{ "${a}_hour" } <=> $resources->{ "${b}_hour" } } @owe;
$recycle{ $focus } = $recycle_capacity;
}
elsif ( $opts{'recycle-by-amount'} ) {
my ($focus) =
map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_ => ($resources->{"${_}_stored"}/$resources->{"${_}_capacity"}) ] }
grep { $producing{ $_ } }
@owe;
$recycle{ $focus } = $recycle_capacity;
}
else { # default to balanced recycling
%recycle = map { $_ => int( $recycle_capacity / $production_count ) * $producing{$_}||0 } @owe;
}
for my $res ( @owe ) {
verbose("we want to make $recycle{$res} units of $res");
$recycled{ $res } += $recycle{ $res };
}
eval {
$client->recycle_recycle(
$center->{id},
$recycle{water}, $recycle{ore}, $recycle{energy},
$center->{url},
) unless $opts{'dry-run'};
};
if ($@) {
warn $@;
next;
}
output("Recycled for $recycle{ore} ore, $recycle{water} water, and $recycle{energy} energy.");
$left_to_recycle -= $recycle_capacity;
}
# TODO $self->cache_invalidate( type => 'body_status', id => $s->{id} );
return \%recycled;
}