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