Package horizons :: Package command :: Module building
[hide private]
[frames] | no frames]

Source Code for Module horizons.command.building

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  from collections import defaultdict 
 23   
 24  import horizons.globals 
 25  from horizons.command import Command 
 26  from horizons.command.uioptions import TransferResource 
 27  from horizons.component.storagecomponent import StorageComponent 
 28  from horizons.constants import BUILDINGS, RES 
 29  from horizons.entities import Entities 
 30  from horizons.scenario import CONDITIONS 
 31  from horizons.util.shapes import Point 
 32  from horizons.util.worldobject import WorldObject, WorldObjectNotFound 
33 34 35 -class Build(Command):
36 """Command class that builds an object."""
37 - def __init__(self, building, x, y, island, rotation=45, ship=None, ownerless=False, 38 settlement=None, tearset=None, data=None, action_set_id=None):
39 """Create the command 40 @param building: building class that is to be built or the id of the building class. 41 @param x, y: int coordinates where the object is to be built. 42 @param ship: ship instance 43 @param island: BuildingOwner instance. Might be Island or World. 44 @param settlement: settlement worldid or None 45 @param tearset: set of worldids of objs to tear before building 46 @param data: data required for building construction 47 @param action_set_id: use this particular action set, don't choose at random 48 """ 49 if hasattr(building, 'id'): 50 self.building_class = building.id 51 else: 52 assert isinstance(building, int) 53 self.building_class = building 54 self.ship = None if ship is None else ship.worldid 55 self.x = int(x) 56 self.y = int(y) 57 self.rotation = int(rotation) 58 self.ownerless = ownerless 59 self.island = island.worldid 60 self.settlement = settlement.worldid if settlement is not None else None 61 self.tearset = tearset or set() 62 self.data = data or {} 63 self.action_set_id = action_set_id
64
65 - def __call__(self, issuer=None):
66 """Execute the command 67 @param issuer: the issuer (player, owner of building) of the command 68 """ 69 self.log.debug("Build: building type %s at (%s,%s)", self.building_class, self.x, self.y) 70 71 island = WorldObject.get_object_by_id(self.island) 72 # slightly ugly workaround to retrieve world and session instance via pseudo-singleton 73 session = island.session 74 75 # check once agaion. needed for MP because of the execution delay. 76 buildable_class = Entities.buildings[self.building_class] 77 build_position = buildable_class.check_build(session, Point(self.x, self.y), 78 rotation=self.rotation, 79 check_settlement=issuer is not None, 80 ship=WorldObject.get_object_by_id(self.ship) if self.ship is not None else None, 81 issuer=issuer) 82 83 # it's possible that the build check requires different actions now, 84 # so update our data 85 self.x, self.y = build_position.position.origin.to_tuple() 86 self.rotation = build_position.rotation 87 self.tearset = build_position.tearset 88 89 if build_position.buildable and issuer: 90 # building seems to buildable, check res too now 91 res_sources = [None if self.ship is None else WorldObject.get_object_by_id(self.ship), 92 None if self.settlement is None else WorldObject.get_object_by_id(self.settlement)] 93 94 build_position.buildable, missing_res = self.check_resources( 95 {}, buildable_class.costs, issuer, res_sources) 96 if not build_position.buildable: 97 self.log.debug("Build aborted. Seems like circumstances changed during EXECUTIONDELAY.") 98 # TODO: maybe show message to user 99 return 100 101 # collect data before objs are torn 102 # required by e.g. the mines to find out about the status of the resource deposit 103 if hasattr(Entities.buildings[self.building_class], "get_prebuild_data"): 104 bclass = Entities.buildings[self.building_class] 105 self.data.update(bclass.get_prebuild_data(session, Point(self.x, self.y))) 106 107 for worldid in sorted(self.tearset): # make sure iteration is the same order everywhere 108 try: 109 obj = WorldObject.get_object_by_id(worldid) 110 Tear(obj)(issuer=None) # execute right now, not via manager 111 except WorldObjectNotFound: # obj might have been removed already 112 pass 113 114 building = Entities.buildings[self.building_class]( 115 session=session, x=self.x, y=self.y, rotation=self.rotation, 116 island=island, action_set_id=self.action_set_id, instance=None, 117 owner=issuer if not self.ownerless else None, 118 **self.data 119 ) 120 building.initialize(**self.data) 121 # initialize must be called immediately after the construction 122 # the building is not usable before this call 123 124 island.add_building(building, issuer) 125 126 if self.settlement is not None: 127 secondary_resource_source = WorldObject.get_object_by_id(self.settlement) 128 elif self.ship is not None: 129 secondary_resource_source = WorldObject.get_object_by_id(self.ship) 130 elif island is not None: 131 secondary_resource_source = island.get_settlement(Point(self.x, self.y)) 132 133 if issuer: # issuer is None if it's a global game command, e.g. on world setup 134 for (resource, value) in building.costs.items(): 135 # remove from issuer, and remove rest from secondary source (settlement or ship) 136 inventory = issuer.get_component(StorageComponent).inventory 137 first_source_remnant = inventory.alter(resource, -value) 138 if first_source_remnant != 0 and secondary_resource_source is not None: 139 inventory = secondary_resource_source.get_component(StorageComponent).inventory 140 second_source_remnant = inventory.alter(resource, first_source_remnant) 141 assert second_source_remnant == 0 142 else: # first source must have covered everything 143 assert first_source_remnant == 0 144 145 # building is now officially built and existent 146 building.start() 147 148 # unload the remaining resources on the human player ship if we just founded a new settlement 149 from horizons.world.player import HumanPlayer 150 if (building.id == BUILDINGS.WAREHOUSE 151 and isinstance(building.owner, HumanPlayer) 152 and horizons.globals.fife.get_uh_setting("AutoUnload")): 153 ship = WorldObject.get_object_by_id(self.ship) 154 ship_inv = ship.get_component(StorageComponent).inventory 155 settlement_inv = building.settlement.get_component(StorageComponent).inventory 156 # copy the inventory first because otherwise we would modify it while iterating 157 for res, amount in ship_inv.get_dump().items(): 158 amount = min(amount, settlement_inv.get_free_space_for(res)) 159 # execute directly, we are already in a command 160 TransferResource(amount, res, ship, building.settlement)(issuer=issuer) 161 162 # NOTE: conditions are not MP-safe! no problem as long as there are no MP-scenarios 163 session.scenario_eventhandler.schedule_check(CONDITIONS.building_num_of_type_greater) 164 165 return building
166 167 @staticmethod
168 - def check_resources(needed_res, costs, issuer, res_sources):
169 """Check if there are enough resources available to cover the costs 170 @param needed_res: awkward dict from BuildingTool.preview_build, use {} everywhere else 171 @param costs: building costs (as in buildingclass.costs) 172 @param issuer: player that builds the building 173 @param res_sources: list of objects with inventory attribute. None values are discarded. 174 @return tuple(bool, missing_resource), True means buildable""" 175 for resource in costs: 176 needed_res[resource] = needed_res.get(resource, 0) + costs[resource] 177 178 reserved_res = defaultdict(int) # res needed for sth else but still present 179 if hasattr(issuer.session.manager, "get_builds_in_construction"): 180 # mp game, consider res still to be subtracted 181 builds = issuer.session.manager.get_builds_in_construction() 182 for build in builds: 183 reserved_res.update(Entities.buildings[build.building_class].costs) 184 185 for resource in needed_res: 186 # check player, ship and settlement inventory 187 available_res = 0 188 # player 189 if resource == RES.GOLD: 190 player_inventory = issuer.get_component(StorageComponent).inventory 191 available_res += player_inventory[resource] 192 # ship or settlement 193 for res_source in res_sources: 194 if res_source is not None: 195 inventory = res_source.get_component(StorageComponent).inventory 196 available_res += inventory[resource] 197 198 if (available_res - reserved_res[resource]) < needed_res[resource]: 199 return (False, resource) 200 return (True, None)
201 202 203 Command.allow_network(Build) 204 Command.allow_network(set)
205 206 207 -class Tear(Command):
208 """Command class that tears an object."""
209 - def __init__(self, building):
210 """Create the command 211 @param building: building that is to be teared. 212 """ 213 self.building = building.worldid
214 215 @classmethod
216 - def additional_removals_after_tear(cls, building_to_remove):
217 """ 218 Calculate which buildings need to be removed when removing the building from its settlement 219 @return tupel(buildings_to_remove, obsolete_settlement_coords) 220 """ 221 settlement = building_to_remove.settlement 222 position = building_to_remove.position 223 # Find all range affecting buildings. 224 other_range_buildings = [] 225 for building in settlement.buildings: 226 if building.id in BUILDINGS.EXPAND_RANGE: 227 other_range_buildings.append(building) 228 other_range_buildings.remove(building_to_remove) 229 230 # Calculate which coordinates are in the new settlement and which are not 231 new_settlement_coords = set() 232 for building in other_range_buildings: 233 range_coords = list(building.position.get_radius_coordinates(building.radius, include_self=True)) 234 new_settlement_coords.update(range_coords) 235 obsolete_settlement_coords = set(settlement.ground_map.keys()).difference(new_settlement_coords) 236 237 # Find the buildings that need to be destroyed 238 buildings_to_destroy = [] 239 for building in settlement.buildings: 240 if building.id in (BUILDINGS.FISH_DEPOSIT, BUILDINGS.CLAY_DEPOSIT, BUILDINGS.STONE_DEPOSIT, BUILDINGS.TREE, BUILDINGS.MOUNTAIN): 241 continue 242 if building.position == position: 243 continue 244 for coord in building.position: 245 if coord in obsolete_settlement_coords: 246 buildings_to_destroy.append(building) 247 break 248 249 return (buildings_to_destroy, obsolete_settlement_coords)
250
251 - def __call__(self, issuer):
252 """Execute the command 253 @param issuer: the issuer of the command 254 """ 255 try: 256 building = WorldObject.get_object_by_id(self.building) 257 except WorldObjectNotFound: 258 self.log.debug("Tear: building %s already gone, not tearing it again.", self.building) 259 return # invalid command, possibly caused by mp delay 260 if building is None or building.fife_instance is None: 261 self.log.error("Tear: attempting to tear down a building that shouldn't exist %s", building) 262 else: 263 self.log.debug("Tear: tearing down %s", building) 264 building.remove()
265 266 267 Command.allow_network(Tear) 268