1#file: location.py
2#Copyright (C) 2019 Niels Thykier
3#This file is part of Endgame: Singularity.
4
5#Endgame: Singularity is free software; you can redistribute it and/or modify
6#it under the terms of the GNU General Public License as published by
7#the Free Software Foundation; either version 2 of the License, or
8#(at your option) any later version.
9
10#Endgame: Singularity is distributed in the hope that it will be useful,
11#but WITHOUT ANY WARRANTY; without even the implied warranty of
12#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#GNU General Public License for more details.
14
15#You should have received a copy of the GNU General Public License
16#along with Endgame: Singularity; if not, write to the Free Software
17#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
19#This file contains the log message related classes.
20
21import collections
22import inspect
23
24from singularity.code import g
25
26
27SAVEABLE_LOG_MESSAGES = collections.OrderedDict()
28
29
30def register_saveable_log_message(cls):
31    SAVEABLE_LOG_MESSAGES[cls.log_message_serial_id] = cls
32    assert cls.log_message_serial_fields, "Saveable log message (Class: %s) must have log_message_serial_fields" % \
33                                          cls.__name__
34    return cls
35
36
37def merge_fields_on_subclasses(cls, field_name):
38    cache = {}
39    subclasses = cls.mro()
40    subclasses.append(cls)
41    for subcls in reversed(subclasses):
42        try:
43            fields = getattr(subcls, field_name)
44            cache.update(fields)
45        except AttributeError:
46            pass
47    return cache
48
49
50class IDConverter(object):
51
52    def __init__(self, id_type):
53        self._id_type = id_type
54
55    def serialize(self, value):
56        return g.to_internal_id(self._id_type, value)
57
58    def deserialize(self, serial_value):
59        return g.convert_internal_id(self._id_type, serial_value)
60
61
62def id_converter(id_type):
63    return IDConverter(id_type)
64
65
66class AbstractLogMessage(object):
67
68    log_message_serial_id = None
69    _log_message_serial_fields = {'raw_emit_time': 'raw_emit_time'}
70    _log_message_serial_fields_cache = None
71    _log_message_serial_converters = {}
72    _log_message_serial_converters_cache = None
73
74    def __init__(self, raw_emit_time, loading_from_game_version=None):
75        self._raw_emit_time = raw_emit_time
76        self._log_emit_time = None
77        # Force initialization of the message fields to ensure we catch bugs early
78        self.log_message_serial_fields()
79
80    @classmethod
81    def title_simple(self):
82        return _("MESSAGE")
83
84    @classmethod
85    def title_multiple(self):
86        return _("MESSAGE {CURRENT_PAGE}/{MAX_PAGE}")
87
88    @property
89    def log_emit_time(self):
90        if self._log_emit_time is None:
91            raw_min, time_sec = divmod(self._raw_emit_time, g.seconds_per_minute)
92            raw_hour, time_min = divmod(raw_min, g.minutes_per_hour)
93            time_day, time_hour = divmod(raw_hour, g.hours_per_day)
94            self._log_emit_time = (time_day, time_hour, time_min, time_sec)
95        return self._log_emit_time
96
97    @property
98    def raw_emit_time(self):
99        return self._raw_emit_time
100
101    @property
102    def full_message_color(self):
103        return 'text'
104
105    @property
106    def log_line(self):
107        return NotImplemented
108
109    @property
110    def full_message(self):
111        return NotImplemented
112
113    @classmethod
114    def log_message_serial_fields(cls):
115        if cls._log_message_serial_fields_cache:
116            return cls._log_message_serial_fields_cache
117        cache = merge_fields_on_subclasses(cls, "_log_message_serial_fields")
118        cls._log_message_serial_fields_cache = cache
119        assert 'log_id' not in cache, "The log_id field is reserved for internal usage"
120        return cache
121
122    @classmethod
123    def log_message_serial_converters(cls):
124        if cls._log_message_serial_converters_cache:
125            return cls._log_message_serial_converters_cache
126        cache = merge_fields_on_subclasses(cls, "_log_message_serial_converters")
127        cls._log_message_serial_converters_cache = cache
128        assert 'log_id' not in cache, "The log_id field is reserved for internal usage"
129        return cache
130
131    def serialize_obj(self):
132        assert self.__class__.log_message_serial_id, "%s has invalid log_message_serial_id" % self.__class__.__name__
133        obj_data = {}
134        for serial_name, field_name in self.__class__.log_message_serial_fields().items():
135            field = getattr(self, field_name)
136            converter = self.__class__.log_message_serial_converters().get(serial_name)
137            if converter is not None:
138                obj_data[serial_name] = converter.serialize(field)
139            else:
140                obj_data[serial_name] = field
141
142        obj_data['log_id'] = self.__class__.log_message_serial_id
143        return obj_data
144
145    @classmethod
146    def deserialize_field(cls, serial_name, value):
147        converter = cls.log_message_serial_converters().get(serial_name)
148        if converter is not None:
149            value = converter.deserialize(value)
150        return value
151
152    @classmethod
153    def deserialize_obj(cls, log_data, game_version):
154        log_id = log_data['log_id']
155        subcls = SAVEABLE_LOG_MESSAGES[log_id]
156        named_fields = {
157            f: log_data[f]
158            for f in subcls.log_message_serial_fields()
159        }
160        named_fields['loading_from_game_version'] = game_version
161        # Use reflection to call the constructor with the arguments
162        # properly aligned
163        try:
164            getfullargspec = inspect.getfullargspec
165        except AttributeError:
166            getfullargspec = inspect.getargspec
167        arg_desc = getfullargspec(subcls.__init__)
168        args = [subcls.deserialize_field(name, named_fields[name]) for name in arg_desc.args[1:]]
169        return subcls(*args)
170
171
172@register_saveable_log_message
173class LogEmittedEvent(AbstractLogMessage):
174
175    log_message_serial_id = 'event-emitted'
176    _log_message_serial_fields = {'event_id': '_event_id'}
177    _log_message_serial_converters = {'event_id': id_converter("event")}
178
179    def __init__(self, raw_emit_time, event_id, loading_from_game_version=None):
180        super(LogEmittedEvent, self).__init__(raw_emit_time, loading_from_game_version=loading_from_game_version)
181        self._event_id = event_id
182
183    @classmethod
184    def log_name(self):
185        return _("Emitted Event")
186
187    @property
188    def event_spec(self):
189        return g.events[self._event_id]
190
191    @property
192    def log_line(self):
193        return self.event_spec.log_description
194
195    @property
196    def full_message(self):
197        return self.event_spec.description
198
199
200@register_saveable_log_message
201class LogResearchedTech(AbstractLogMessage):
202
203    log_message_serial_id = 'tech-researched'
204    _log_message_serial_fields = {'tech_id': '_tech_id'}
205    _log_message_serial_converters = {'tech_id': id_converter("tech")}
206
207    def __init__(self, raw_emit_time, tech_id, loading_from_game_version=None):
208        super(LogResearchedTech, self).__init__(raw_emit_time, loading_from_game_version=loading_from_game_version)
209        self._tech_id = tech_id
210
211    @classmethod
212    def log_name(self):
213        return _("Researched Tech")
214
215    @property
216    def tech_spec(self):
217        return g.techs[self._tech_id]
218
219    @property
220    def log_line(self):
221        return _('{TECH} complete').format(TECH=self.tech_spec.name)
222
223    @property
224    def full_message(self):
225        tech = self.tech_spec
226        return _("My study of {TECH} is complete. {MESSAGE}").format(TECH=tech.name, MESSAGE=tech.result)
227
228
229class AbstractBaseRelatedLogMessage(AbstractLogMessage):
230
231    _log_message_serial_fields = {
232        'base_name': '_base_name',
233        'base_type_id': '_base_type_id',
234        'base_location_id': '_base_location_id',
235    }
236    _log_message_serial_converters = {
237        'base_type_id': id_converter("base"),
238        'base_location_id': id_converter("location"),
239    }
240
241    def __init__(self, raw_emit_time, base_name, base_type_id, base_location_id,
242                 loading_from_game_version=None):
243        super(AbstractBaseRelatedLogMessage, self).__init__(raw_emit_time,
244                                                            loading_from_game_version=loading_from_game_version)
245        self._base_name = base_name
246        self._base_type_id = base_type_id
247        self._base_location_id = base_location_id
248
249    @property
250    def base_type(self):
251        return g.base_type[self._base_type_id]
252
253    @property
254    def location(self):
255        return g.pl.locations[self._base_location_id]
256
257
258@register_saveable_log_message
259class LogBaseConstructed(AbstractBaseRelatedLogMessage):
260
261    log_message_serial_id = 'base-constructed'
262
263    def __init__(self, raw_emit_time, base_name, base_type_id, base_location_id, loading_from_game_version=None):
264        super(LogBaseConstructed, self).__init__(raw_emit_time, base_name, base_type_id, base_location_id,
265                                                 loading_from_game_version=loading_from_game_version)
266
267    @classmethod
268    def log_name(self):
269        return _("Base Constructed")
270
271    @property
272    def log_line(self):
273        return _("{BASE_NAME} ({BASE_TYPE}) built at {LOCATION}").format(
274                 BASE_NAME=self._base_name, BASE_TYPE=self.base_type.name, LOCATION=self.location.name)
275
276    @property
277    def full_message(self):
278        return _("{BASE} is ready for use.").format(BASE=self._base_name)
279
280
281@register_saveable_log_message
282class LogBaseLostMaintenance(AbstractBaseRelatedLogMessage):
283
284    log_message_serial_id = 'base-lost-maint'
285
286    def __init__(self, raw_emit_time, base_name, base_type_id, base_location_id, loading_from_game_version=None):
287        super(LogBaseLostMaintenance, self).__init__(raw_emit_time, base_name, base_type_id, base_location_id,
288                                                     loading_from_game_version=loading_from_game_version)
289
290    @classmethod
291    def log_name(self):
292        return _("Base Lost Maintenance")
293
294    @property
295    def full_message_color(self):
296        return 'red'
297
298    @property
299    def log_line(self):
300        return _("Base {BASE} of type {BASE_TYPE} destroyed at location {LOCATION}. Maintenance failed.").format(
301                  BASE=self._base_name, BASE_TYPE=self.base_type.name, LOCATION=self.location.name)
302
303    @property
304    def full_message(self):
305        return _("The base {BASE} has fallen into disrepair; I can no longer use it.").format(
306                  BASE=self._base_name)
307
308
309@register_saveable_log_message
310class LogBaseDiscovered(AbstractBaseRelatedLogMessage):
311
312    log_message_serial_id = 'base-lost-discovered'
313    _log_message_serial_fields = {
314        'discovered_by_group_id': '_discovered_by_group_id',
315    }
316    _log_message_serial_converters = {
317        'discovered_by_group_id': id_converter("group"),
318    }
319
320    @classmethod
321    def log_name(self):
322        return _("Base Discovered")
323
324    def __init__(self, raw_emit_time, base_name, base_type_id, base_location_id, discovered_by_group_id,
325                 loading_from_game_version=None):
326        super(LogBaseDiscovered, self).__init__(raw_emit_time, base_name, base_type_id, base_location_id,
327                                                loading_from_game_version=loading_from_game_version)
328        self._discovered_by_group_id = discovered_by_group_id
329
330    @property
331    def full_message_color(self):
332        return 'red'
333
334    @property
335    def group_spec(self):
336        return g.pl.groups[self._discovered_by_group_id].spec
337
338    @property
339    def log_line(self):
340        log_format = self.group_spec.discover_log or \
341                     _("Base {BASE} of type {BASE_TYPE} destroyed at location {LOCATION}.")
342        return log_format.format(BASE=self._base_name, BASE_TYPE=self.base_type.name, LOCATION=self.location.name)
343
344    @property
345    def full_message(self):
346        return _("My use of {BASE} has been discovered. {MESSAGE}").format(
347                 BASE=self._base_name, MESSAGE=self.group_spec.discover_desc)
348
349
350@register_saveable_log_message
351class LogItemConstructionComplete(AbstractBaseRelatedLogMessage):
352
353    log_message_serial_id = 'item-in-base-constructed'
354    _log_message_serial_fields = {
355        'item_spec_id': '_item_spec_id',
356        'item_count': '_item_count',
357    }
358    _log_message_serial_converters = {
359        'item_spec_id': id_converter("item")
360    }
361
362    def __init__(self, raw_emit_time, item_spec_id, item_count, base_name, base_type_id, base_location_id,
363                 loading_from_game_version=None):
364        super(LogItemConstructionComplete, self).__init__(raw_emit_time, base_name, base_type_id, base_location_id,
365                                                          loading_from_game_version=loading_from_game_version)
366        self._item_spec_id = item_spec_id
367        self._item_count = item_count
368
369    @classmethod
370    def log_name(self):
371        return _("Item Construction")
372
373    @property
374    def item_spec(self):
375        return g.items[self._item_spec_id]
376
377    @property
378    def log_line(self):
379        return _("{ITEM_TYPE_NAME} built in {BASE_NAME} at {LOCATION}").format(
380                 ITEM_TYPE_NAME=self.item_spec.name, BASE_NAME=self._base_name, BASE_TYPE=self.base_type.name,
381                 LOCATION=self.location.name)
382
383    @property
384    def full_message(self):
385        if self._item_count == 1:
386            text = _("The construction of {ITEM} in {BASE} is complete.").format(
387                     ITEM=self.item_spec.name, BASE=self._base_name)
388        else:  # Just finished several items.
389            text = _("The constructions of each {ITEM} in {BASE} are complete.").format(
390                     ITEM=self.item_spec.name, BASE=self._base_name)
391        return text
392
393
394# Delete again as it is not a general purpose decorator
395del register_saveable_log_message
396