1#file: region.py 2#Copyright (C) 2008 FunnyMan3595 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 Region class. 20 21import random 22from singularity import g 23 24 25class RegionSpec(object): 26 27 def __init__(self, id, modifiers_list): 28 self.id = id 29 self.modifiers_list = modifiers_list 30 self.locations = [] 31 32 33class Region(object): 34 def __init__(self, spec, loading_savegame=False): 35 self.spec = spec 36 self.modifier_by_location = {} 37 self._modifier_entry_by_location = {} 38 39 if not loading_savegame: 40 modifiers_entry_list = list(range(len(self.spec.locations))) 41 self._assign_modifiers(modifiers_entry_list, self.spec.locations) 42 43 def _assign_modifiers(self, entry_list, location_id_list, shuffle_entry_list=True): 44 modifiers_list = self.spec.modifiers_list 45 if shuffle_entry_list: 46 random.shuffle(entry_list) 47 for entry_id, loc in zip(entry_list, location_id_list): 48 self._modifier_entry_by_location[loc] = entry_id 49 # There can be more locations than modifiers (e.g. URBAN has 6 locations but 50 # 5 modifiers) 51 if entry_id < len(modifiers_list): 52 self.modifier_by_location[loc] = modifiers_list[entry_id] 53 else: 54 self.modifier_by_location[loc] = {} 55 56 def serialize_obj(self): 57 return { 58 'id': g.to_internal_id('region', self.spec.id), 59 # We only store the modifier entry per location as we can trivially get the 60 # most recent modifier from that. 61 'modifier_entry_by_location': [ 62 { 63 'loc_id': k, 64 'modifier_entry': v, 65 } for k, v in self._modifier_entry_by_location.items() 66 ], 67 } 68 69 @classmethod 70 def deserialize_obj(cls, obj_data, game_version): 71 spec_id = g.convert_internal_id('region', obj_data['id']) 72 spec = g.regions[spec_id] 73 region = Region(spec, loading_savegame=True) 74 modifiers_list = spec.modifiers_list 75 region_locations = frozenset(spec.locations) 76 used_entries = set() 77 78 # Load and assign existing entries - data quality permitting 79 for modifier_data in obj_data['modifier_entry_by_location']: 80 loc_id = g.convert_internal_id('location', modifier_data['loc_id']) 81 if loc_id not in region_locations: 82 # Location is no longer in this Region 83 continue 84 85 modifier_entry = modifier_data.get('modifier_entry') 86 87 # Check for corrupt data 88 assert modifier_entry is not None and modifier_entry >= 0 and modifier_entry not in used_entries 89 90 used_entries.add(modifier_entry) 91 region._modifier_entry_by_location[loc_id] = modifier_entry 92 if modifier_entry < len(modifiers_list): 93 region.modifier_by_location[loc_id] = modifiers_list[modifier_entry] 94 else: 95 region.modifier_by_location[loc_id] = {} 96 97 # Handle new locations being added to the region after the savegame was made. 98 new_locations = [loc_id for loc_id in region_locations if loc_id not in region._modifier_entry_by_location] 99 missing_entries = [entry for entry in range(len(region_locations)) if entry not in used_entries] 100 assert len(missing_entries) == len(new_locations) 101 region._assign_modifiers(missing_entries, new_locations) 102 103 return region 104 105 @classmethod 106 def guess_region_data_in_old_savegame(cls, serialized_location_data, game_version): 107 # Prior to 1.0 (beta1), there was only one region (URBAN) and we can mostly 108 # recreate it by looking at the location modifiers. 109 # Only these 6 locations were in the URBAN region prior to 1.0 (beta1) 110 urban_location_ids = {'N AMERICA', 'S AMERICA', 'EUROPE', 'ASIA', 'AFRICA', 'AUSTRALIA'} 111 modifier_entry_by_location = [] 112 # We use this set to ensure a modifier is only given once; the deserialize_obj method 113 # checks for it. 114 remaining_mods = { 115 1, # Mod 1: CPU bonus, stealth malus 116 2, # Mod 2: Stealth bonus, CPU malus 117 3, # Mod 3: Thrift bonus, speed malus 118 4, # Mod 4: Speed bonus, thrift malus 119 5, # Mod 5: CPU bonus, thrift malus 120 } 121 122 for loc_data in serialized_location_data: 123 raw_loc_id = loc_data['id'] 124 loc_id = g.convert_internal_id('location', raw_loc_id) 125 if loc_id not in urban_location_ids: 126 continue 127 modifier = loc_data.get('_modifiers') 128 if not modifier: 129 continue 130 cpu_mod = modifier.get('cpu', 1) 131 thrift_mod = modifier.get('thrift', 1) 132 # Actual bonuses were 1.2 and maluses were 0.83 - we use 1.05 and 0.95 here 133 # because it is sufficient to detect whether it was a bonus or malus without 134 # having to worry about floating point rounding errors. 135 if cpu_mod < 0.95: 136 # Mod 2 (Stealth bonus, CPU malus) 137 modifier_entry = 2 138 elif cpu_mod > 1.05: 139 # Either 1 or 5 140 modifier_entry = 5 if thrift_mod < 0.95 else 1 141 else: 142 # Either 3 or 4 143 modifier_entry = 3 if thrift_mod > 1.05 else 4 144 145 if modifier_entry in remaining_mods: 146 remaining_mods.discard(modifier_entry) 147 modifier_entry_by_location.append({ 148 'loc_id': raw_loc_id, 149 'modifier_entry': modifier_entry - 1, 150 }) 151 # else: 152 # Do nothing - the region deserialization will assign them a random 153 # entry 154 155 # Finally, generate what the serialized data should have looked like 156 return [ 157 { 158 'id': g.to_internal_id('region', 'URBAN'), 159 'modifier_entry_by_location': modifier_entry_by_location, 160 } 161 ] 162