1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import logging
24 import operator
25 from collections import namedtuple
26
27 from horizons.component.ambientsoundcomponent import AmbientSoundComponent
28 from horizons.component.restrictedpickup import RestrictedPickup
29 from horizons.component.storagecomponent import StorageComponent
30 from horizons.constants import COLLECTORS
31 from horizons.ext.enum import Enum
32 from horizons.scheduler import Scheduler
33 from horizons.util.pathfinding import PathBlockedError
34 from horizons.util.python import decorators
35 from horizons.util.python.callback import Callback
36 from horizons.util.worldobject import WorldObject
37 from horizons.world.units.unit import Unit
41 """Base class for every collector. Does not depend on any home building.
42
43 Timeline:
44 * search_job
45 * * get_job
46 * * handle_no_possible_job
47 * * begin_current_job
48 * * * setup_new_job
49 * * * move to target
50 on arrival there:
51 * begin_working
52 after some pretended working:
53 * finish_working
54 * * transfer_res
55 after subclass has done actions to finish job:
56 * end_job
57 """
58 log = logging.getLogger("world.units.collector")
59
60 work_duration = COLLECTORS.DEFAULT_WORK_DURATION
61 destination_always_in_building = False
62
63
64
65
66 states = Enum('idle',
67 'moving_to_target',
68 'working',
69 'moving_home',
70 'waiting_for_animal_to_stop',
71 'waiting_for_herder',
72 'no_job_walking_randomly',
73 'no_job_waiting',
74
75 'decommissioned',
76 )
77
78
79
80 - def __init__(self, x, y, slots=1, start_hidden=True, **kwargs):
89
90 - def __init(self, state, start_hidden):
91 self.state = state
92 self.start_hidden = start_hidden
93 if self.start_hidden:
94 self.hide()
95
96 self.job = None
97
99 """Removes the instance. Useful when the home building is destroyed"""
100 self.log.debug("%s: remove called", self)
101 self.cancel(continue_action=lambda: 42)
102
103 self._abort_collector_job()
104 self.hide()
105 self.job = None
106 super().remove()
107
109 if self.job is None or self.state == self.states.moving_home:
110
111
112 return
113 if not hasattr(self.job.object, 'remove_incoming_collector'):
114
115
116 return
117 self.job.object.remove_incoming_collector(self)
118
119
120
121 - def save(self, db):
122 super().save(db)
123
124
125
126 current_callback = None
127 remaining_ticks = None
128 if self.state == self.states.idle:
129 current_callback = self.search_job
130 elif self.state == self.states.working:
131 current_callback = self.finish_working
132 if current_callback is not None:
133 calls = Scheduler().get_classinst_calls(self, current_callback)
134 assert len(calls) == 1, 'Collector should have callback {} scheduled, but has {}'.format(
135 current_callback, [str(i) for i in Scheduler().get_classinst_calls(self).keys()])
136 remaining_ticks = max(list(calls.values())[0], 1)
137
138 db("INSERT INTO collector(rowid, state, remaining_ticks, start_hidden) VALUES(?, ?, ?, ?)",
139 self.worldid, self.state.index, remaining_ticks, self.start_hidden)
140
141
142 if self.job is not None:
143 obj_id = -1 if self.job.object is None else self.job.object.worldid
144
145
146 for entry in self.job.reslist:
147 db("INSERT INTO collector_job(collector, object, resource, amount) VALUES(?, ?, ?, ?)",
148 self.worldid, obj_id, entry.res, entry.amount)
149
150 - def load(self, db, worldid):
151 super().load(db, worldid)
152
153
154 state_id, remaining_ticks, start_hidden = \
155 db("SELECT state, remaining_ticks, start_hidden FROM collector \
156 WHERE rowid = ?", worldid)[0]
157 self.__init(self.states[state_id], start_hidden)
158
159
160 job_db = db("SELECT object, resource, amount FROM collector_job WHERE collector = ?", worldid)
161 if job_db:
162 reslist = []
163 for obj, res, amount in job_db:
164 reslist.append(Job.ResListEntry(res, amount, False))
165
166
167 self.job = Job(obj, reslist)
168
169 def fix_job_object():
170
171 if self.job:
172 if self.job.object == -1:
173 self.job.object = None
174 else:
175 self.job.object = WorldObject.get_object_by_id(self.job.object)
176
177
178 Scheduler().add_new_object(
179 Callback.ChainedCallbacks(
180 fix_job_object,
181 Callback(self.apply_state, self.state, remaining_ticks)),
182 self, run_in=0
183 )
184
206
207
208
210 """Returns inventory where collected res will be stored.
211 This could be the inventory of a home_building, or it's own.
212 """
213 raise NotImplementedError
214
216 """Returns a list of collectors, that work for the same "inventory"."""
217 return []
218
220 """Return all resources the collector can collect"""
221 raise NotImplementedError
222
224 """Returns the next job or None"""
225 raise NotImplementedError
226
227
229 """Search for a job, only called if the collector does not have a job.
230 If no job is found, a new search will be scheduled in a few ticks."""
231 self.job = self.get_job()
232 if self.job is None:
233 self.handle_no_possible_job()
234 else:
235 self.begin_current_job()
236
241
245
261
263 """Checks out if we could get res from target.
264 Does _not_ check for anything else (e.g. if we are able to walk there).
265 @param target: possible target. buildings are supported, support for more can be added.
266 @param res: resource id
267 @return: instance of Job or None, if we can't collect anything
268 """
269 res_amount = target.get_available_pickup_amount(res, self)
270 if res_amount <= 0:
271
272 return None
273
274
275
276 total_registered_amount_consumer = sum(
277 entry.amount for
278 collector in self.get_colleague_collectors() if
279 collector.job is not None for
280 entry in collector.job.reslist if
281 entry.res == res)
282
283 inventory = self.get_home_inventory()
284
285
286 home_inventory_free_space = inventory.get_free_space_for(res) \
287 - total_registered_amount_consumer
288 if home_inventory_free_space <= 0:
289
290 return None
291
292 collector_inventory_free_space = self.get_component(StorageComponent).inventory.get_free_space_for(res)
293 if collector_inventory_free_space <= 0:
294
295 return None
296
297 possible_res_amount = min(res_amount, home_inventory_free_space,
298 collector_inventory_free_space)
299
300 target_inventory_full = (target.get_component(StorageComponent).inventory.get_free_space_for(res) == 0)
301
302
303 return Job.ResListEntry(res, possible_res_amount, target_inventory_full)
304
306 """Return best possible job from jobs.
307 "Best" means that the job is highest when the job list was sorted.
308 "Possible" means that we can find a path there.
309 @param jobs: unsorted JobList instance
310 @return: selected Job instance from list or None if no jobs are possible."""
311 jobs.sort_jobs()
312
313 for job in jobs:
314 path = self.check_move(job.object.loading_area)
315 if path:
316 job.path = path
317 return job
318
319 return None
320
322 """Starts executing the current job by registering itself and moving to target.
323 @param job_location: Where collector should work. default: job.object.loading_area"""
324 self.log.debug("%s prepares job %s", self, self.job)
325 self.setup_new_job()
326 self.show()
327 if job_location is None:
328 job_location = self.job.object.loading_area
329 self.move(job_location, self.begin_working,
330 destination_in_building=self.destination_always_in_building,
331 blocked_callback=self.handle_path_to_job_blocked, path=self.job.path)
332 self.state = self.states.moving_to_target
333
340
347
360
383
385 """Transfers resources from target to collector inventory"""
386 new_reslist = []
387 for entry in self.job.reslist:
388 actual_amount = self.job.object.pickup_resources(entry.res, entry.amount, self)
389 if entry.amount != actual_amount:
390 new_reslist.append(Job.ResListEntry(entry.res, actual_amount, False))
391 else:
392 new_reslist.append(entry)
393
394 remnant = self.get_component(StorageComponent).inventory.alter(entry.res, actual_amount)
395 assert remnant == 0, "{} couldn't take all of res {}; remnant: {}; planned: {}".format(
396 self, entry.res, remnant, entry.amount)
397 self.job.reslist = new_reslist
398
407
408
409
420
421 - def cancel(self, continue_action):
422 """Aborts the current job.
423 @param continue_action: Callback, gets called after cancel. Specifies what collector
424 is supposed to do now.
425 NOTE: Subclasses set this to a proper action that makes the collector continue to work.
426 If the collector is supposed to be remove, use a noop.
427 """
428 self.stop()
429 self.log.debug("%s was cancelled, continue action is %s", self, continue_action)
430
431 self._abort_collector_job()
432 if self.job is not None:
433
434 if self.state == self.states.working:
435 removed_calls = Scheduler().rem_call(self, self.finish_working)
436 assert removed_calls == 1, 'removed {} calls instead of one'.format(removed_calls)
437 self.job = None
438 self.state = self.states.idle
439
440
441
442
443
444
445 Scheduler().rem_call(self, self.resume_movement)
446 continue_action()
447
449 try:
450 return super().__str__() + "(state={})".format(self.state)
451 except AttributeError:
452 return super().__str__()
453
456 """Data structure for storing information of collector jobs"""
457 ResListEntry = namedtuple("ResListEntry", ["res", "amount", "target_inventory_full"])
458
460 """
461 @param obj: ResourceHandler that provides res
462 @param reslist: ResListEntry list
463 res: resource to get
464 amount: amount of resource to get
465 target_inventory_full: whether target inventory can't store any more of this res.
466 """
467 for entry in reslist:
468 assert entry.amount >= 0
469
470
471
472
473 self.object = obj
474 self.reslist = reslist
475
476 self.path = None
477
478 @decorators.cachedproperty
480
481 return sum(entry.amount for entry in self.reslist)
482
483 @decorators.cachedproperty
485
486 return [entry.res for entry in self.reslist]
487
488 @decorators.cachedproperty
490
491 return sum(1 for entry in self.reslist if entry.target_inventory_full)
492
494 return "Job({}, {})".format(self.object, self.reslist)
495
498 """Data structure for evaluating best jobs.
499 It's a list extended by special sort functions.
500 """
501 order_by = Enum('rating', 'amount', 'random', 'fewest_available', 'fewest_available_and_distance', 'for_storage_collector', 'distance')
502
503 - def __init__(self, collector, job_order):
504 """
505 @param collector: collector instance
506 @param job_order: instance of order_by-Enum
507 """
508 super().__init__()
509 self.collector = collector
510
511 sort_fun_name = '_sort_jobs_' + str(job_order)
512 if not hasattr(self, sort_fun_name):
513 self._selected_sort_jobs = self._sort_jobs_amount
514 print('WARNING: invalid job order: ', job_order)
515 else:
516 self._selected_sort_jobs = getattr(self, sort_fun_name)
517
519 """Call this to sort jobs.
520
521 The function to call is decided in `__init__`.
522 """
523 self._selected_sort_jobs()
524
528
530 """Sorts the jobs by the amount of resources available"""
531 self.sort(key=operator.attrgetter('amount_sum'), reverse=True)
532
534 """Prefer jobs where least amount is available in obj's inventory.
535 Only considers resource of resource list with minimum amount available.
536 This is supposed to fix urgent shortages."""
537
538 if shuffle_first:
539 self.collector.session.random.shuffle(self)
540 inventory = self.collector.get_home_inventory()
541 self.sort(key=lambda job: min(inventory[res] for res in job.resources), reverse=False)
542
548
554
556 """Prefer targets that are nearer"""
557 collector_point = self.collector.position
558 self.sort(key=lambda job: collector_point.distance(job.object.loading_area))
559
561 """Prefer targets with full inventory"""
562 self.sort(key=operator.attrgetter('target_inventory_full_num'), reverse=True)
563
565 return str([str(i) for i in self])
566