Package horizons :: Package gui :: Package tabs :: Module buildtabs
[hide private]
[frames] | no frames]

Source Code for Module horizons.gui.tabs.buildtabs

  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  import horizons.globals 
 23  from horizons.command.building import Build 
 24  from horizons.component.storagecomponent import StorageComponent 
 25  from horizons.entities import Entities 
 26  from horizons.ext.enum import Enum 
 27  from horizons.gui.tabs.tabinterface import TabInterface 
 28  from horizons.i18n import gettext as T 
 29  from horizons.messaging import NewPlayerSettlementHovered 
 30  from horizons.util.lastactiveplayersettlementmanager import LastActivePlayerSettlementManager 
 31  from horizons.util.python import decorators 
 32  from horizons.util.python.callback import Callback 
 33  from horizons.util.yamlcache import YamlCache 
34 35 36 -class InvalidBuildMenuFileFormat(Exception):
37 pass
38
39 40 -class BuildTab(TabInterface):
41 """ 42 Layout data is defined in image_data and text_data. 43 Columns in the tabs are enumerated as follows: 44 01 11 21 31 45 02 12 22 32 46 03 13 23 33 47 04 14 24 34 48 Boxes and Labels have the same number as their left upper icon. 49 Check buildtab.xml for details. Icons without image are transparent. 50 """ 51 lazy_loading = True 52 widget = 'buildtab.xml' 53 54 MAX_ROWS = 4 55 MAX_COLS = 4 56 57 build_menus = [ 58 "content/objects/gui_buildmenu/build_menu_per_tier.yaml", 59 "content/objects/gui_buildmenu/build_menu_per_type.yaml" 60 ] 61 62 layout_per_tier_index = 0 63 layout_per_type_index = 1 64 build_menu_config_per_tier = build_menus[layout_per_tier_index] 65 build_menu_config_per_type = build_menus[layout_per_type_index] 66 67 # NOTE: check for occurrences of this when adding one, you might want to 68 # add respective code there as well 69 unlocking_strategies = Enum("tab_per_tier", # 1 tab per tier 70 "single_per_tier" # each single building unlocked if tier is unlocked 71 ) 72 73 last_active_build_tab = None # type: int 74
75 - def __init__(self, session, tabindex, data, build_callback, unlocking_strategy, build_menu_config):
76 """ 77 @param tabindex: position of tab 78 @param data: data directly from yaml specifying the contents of this tab 79 @param build_callback: called on clicks 80 @param unlocking_strategy: element of unlocking_strategies 81 @param build_menu_config: entry of build_menus where this definition originates from 82 """ 83 icon_path = None 84 helptext = None 85 headline = None 86 rows = [] 87 for entry in data: 88 if isinstance(entry, dict): 89 # this is one key-value pair, e.g. "- icon: img/foo.png" 90 if len(entry) != 1: 91 raise InvalidBuildMenuFileFormat("Invalid entry in buildmenuconfig: {}".format(entry)) 92 key, value = list(entry.items())[0] 93 if key == "icon": 94 icon_path = value 95 elif key == "helptext": 96 helptext = value[2:] if value.startswith('_ ') else value 97 elif key == "headline": 98 headline = value[2:] if value.startswith('_ ') else value 99 else: 100 raise InvalidBuildMenuFileFormat( 101 "Invalid key: {}\nMust be either icon, helptext or headline.".format(key)) 102 elif isinstance(entry, list): 103 # this is a line of data 104 rows.append(entry) # parse later on demand 105 else: 106 raise InvalidBuildMenuFileFormat("Invalid entry: {}".format(entry)) 107 108 if not icon_path: 109 raise InvalidBuildMenuFileFormat("icon_path definition is missing.") 110 111 self.session = session 112 self.tabindex = tabindex 113 self.build_callback = build_callback 114 self.unlocking_strategy = unlocking_strategy 115 if self.unlocking_strategy != self.__class__.unlocking_strategies.tab_per_tier: 116 if not helptext and not headline: 117 raise InvalidBuildMenuFileFormat("helptext definition is missing.") 118 self.row_definitions = rows 119 self.headline = T(headline) if headline else headline # don't translate None 120 self.helptext = T(helptext) if helptext else self.headline 121 122 super().__init__(icon_path=icon_path)
123 124 @classmethod
125 - def get_saved_buildstyle(cls):
126 saved_build_style = horizons.globals.fife.get_uh_setting("Buildstyle") 127 return cls.build_menus[saved_build_style]
128
129 - def init_widget(self):
130 self.__current_settlement = None 131 headline_lbl = self.widget.child_finder('headline') 132 if self.headline: # prefer specific headline 133 headline_lbl.text = self.headline 134 elif self.unlocking_strategy == self.__class__.unlocking_strategies.tab_per_tier: 135 headline_lbl.text = T(self.session.db.get_settler_name(self.tabindex))
136
137 - def set_content(self):
138 """Parses self.row_definitions and sets the content accordingly""" 139 settlement = LastActivePlayerSettlementManager().get() 140 141 def _set_entry(button, icon, building_id): 142 """Configure a single build menu button""" 143 if self.unlocking_strategy == self.__class__.unlocking_strategies.single_per_tier and \ 144 self.get_building_tiers()[building_id] > self.session.world.player.settler_level: 145 return 146 147 building = Entities.buildings[building_id] 148 button.helptext = building.get_tooltip() 149 150 # Add necessary resources to tooltip text. 151 # tooltip.py will then place icons from this information. 152 required_resources = '' 153 for resource_id, amount_needed in sorted(building.costs.items()): 154 required_resources += ' {}:{}'.format(resource_id, amount_needed) 155 required_text = '[[Buildmenu{}]]'.format(required_resources) 156 button.helptext = required_text + button.helptext 157 158 enough_res = False # don't show building by default 159 if settlement is not None: # settlement is None when the mouse has left the settlement 160 res_overview = self.session.ingame_gui.resource_overview 161 show_costs = Callback(res_overview.set_construction_mode, settlement, building.costs) 162 button.mapEvents({ 163 button.name + "/mouseEntered/buildtab": show_costs, 164 button.name + "/mouseExited/buildtab": res_overview.close_construction_mode 165 }) 166 167 (enough_res, missing_res) = Build.check_resources({}, building.costs, settlement.owner, [settlement]) 168 # Check whether to disable build menu icon (not enough res available). 169 if enough_res: 170 icon.image = "content/gui/images/buttons/buildmenu_button_bg.png" 171 button.path = "icons/buildmenu/{id:03d}".format(id=building_id) 172 else: 173 icon.image = "content/gui/images/buttons/buildmenu_button_bg_bw.png" 174 button.path = "icons/buildmenu/greyscale/{id:03d}".format(id=building_id) 175 176 button.capture(Callback(self.build_callback, building_id))
177 178 for row_num, row in enumerate(self.row_definitions): 179 # we have integers for building types, strings for headlines above slots and None as empty slots 180 column = -1 # can't use enumerate, not always incremented 181 for entry in row: 182 column += 1 183 position = (10 * column) + (row_num + 1) # legacy code, first row is 1, 11, 21 184 if entry is None: 185 continue 186 elif (column + 1) > self.MAX_COLS: 187 # out of 4x4 bounds 188 err = "Invalid entry '{}': column {} does not exist.".format(entry, column + 1) 189 err += " Max. column amount in current layout is {}.".format(self.MAX_COLS) 190 raise InvalidBuildMenuFileFormat(err) 191 elif row_num > self.MAX_ROWS: 192 # out of 4x4 bounds 193 err = "Invalid entry '{}': row {} does not exist.".format(entry, row_num) 194 err += " Max. row amount in current layout is {}.".format(self.MAX_ROWS) 195 raise InvalidBuildMenuFileFormat(err) 196 elif isinstance(entry, str): 197 column -= 1 # a headline does not take away a slot 198 lbl = self.widget.child_finder('label_{position:02d}'.format(position=position)) 199 lbl.text = T(entry[2:]) if entry.startswith('_ ') else entry 200 elif isinstance(entry, int): 201 button = self.widget.child_finder('button_{position:02d}'.format(position=position)) 202 icon = self.widget.child_finder('icon_{position:02d}'.format(position=position)) 203 _set_entry(button, icon, entry) 204 else: 205 raise InvalidBuildMenuFileFormat("Invalid entry: {}".format(entry))
206
207 - def refresh(self):
208 self.set_content()
209
210 - def on_settlement_change(self, message):
211 if message.settlement is not None: 212 # only react to new actual settlements, else we have no res source 213 self.refresh()
214
215 - def __remove_changelisteners(self):
216 NewPlayerSettlementHovered.discard(self.on_settlement_change) 217 if self.__current_settlement is not None: 218 inventory = self.__current_settlement.get_component(StorageComponent).inventory 219 inventory.discard_change_listener(self.refresh)
220
221 - def __add_changelisteners(self):
222 NewPlayerSettlementHovered.subscribe(self.on_settlement_change) 223 if self.__current_settlement is not None: 224 inventory = self.__current_settlement.get_component(StorageComponent).inventory 225 if not inventory.has_change_listener(self.refresh): 226 inventory.add_change_listener(self.refresh)
227
228 - def show(self):
229 self.__remove_changelisteners() 230 self.__current_settlement = LastActivePlayerSettlementManager().get() 231 self.__add_changelisteners() 232 self.__class__.last_active_build_tab = self.tabindex 233 super().show() 234 235 button = self.widget.child_finder("switch_build_menu_config_button") 236 self._set_switch_layout_button_image(button) 237 button.capture(self._switch_build_menu_config)
238
239 - def hide(self):
240 self.__remove_changelisteners() 241 super().hide()
242
243 - def _set_switch_layout_button_image(self, button):
244 image_path = "content/gui/icons/tabwidget/buildmenu/" 245 if horizons.globals.fife.get_uh_setting("Buildstyle") == self.layout_per_type_index: 246 button.up_image = image_path + "tier.png" 247 else: 248 button.up_image = image_path + "class.png" 249 self.switch_layout_button_needs_update = False
250
251 - def _switch_build_menu_config(self):
252 """Sets next build menu config and recreates the gui""" 253 cur_index = horizons.globals.fife.get_uh_setting("Buildstyle") 254 new_index = (cur_index + 1) % len(self.__class__.build_menus) 255 256 # after switch set active tab to first 257 self.__class__.last_active_build_tab = 0 258 259 #save build style 260 horizons.globals.fife.set_uh_setting("Buildstyle", new_index) 261 horizons.globals.fife.save_settings() 262 self.session.ingame_gui.show_build_menu(update=True)
263 264 @classmethod
265 - def create_tabs(cls, session, build_callback):
266 """Create according to current build menu config 267 @param build_callback: function to call to enable build mode, has to take building type parameter 268 """ 269 source = cls.get_saved_buildstyle() 270 # parse 271 data = YamlCache.get_file(source, game_data=True) 272 if 'meta' not in data: 273 raise InvalidBuildMenuFileFormat('File does not contain "meta" section') 274 metadata = data['meta'] 275 if 'unlocking_strategy' not in metadata: 276 raise InvalidBuildMenuFileFormat('"meta" section does not contain "unlocking_strategy"') 277 try: 278 unlocking_strategy = cls.unlocking_strategies.get_item_for_string(metadata['unlocking_strategy']) 279 except KeyError: 280 raise InvalidBuildMenuFileFormat('Invalid entry for "unlocking_strategy"') 281 282 # create tab instances 283 tabs = [] 284 for tab, tabdata in sorted(data.items()): 285 if tab == "meta": 286 continue # not a tab 287 288 if unlocking_strategy == cls.unlocking_strategies.tab_per_tier and len(tabs) > session.world.player.settler_level: 289 break 290 291 try: 292 tab = BuildTab(session, len(tabs), tabdata, build_callback, unlocking_strategy, source) 293 tabs.append(tab) 294 except Exception as e: 295 to_add = "\nThis error happened in {} of {} .".format(tab, source) 296 e.args = (e.args[0] + to_add, ) + e.args[1:] 297 e.message = (e.message + to_add) 298 raise 299 300 return tabs
301 302 @classmethod 303 @decorators.cachedfunction
304 - def get_building_tiers(cls):
305 """Returns a dictionary mapping building type ids to their tiers 306 @return cached dictionary (don't modify)""" 307 building_tiers = {} 308 data = YamlCache.get_file(cls.build_menu_config_per_tier, game_data=True) 309 tier = -1 310 for tab, tabdata in sorted(data.items()): 311 if tab == "meta": 312 continue # not a tab 313 314 tier += 1 315 316 for row in tabdata: 317 if isinstance(row, list): # actual content 318 for entry in row: 319 if isinstance(entry, int): # actual building button 320 building_tiers[entry] = tier 321 return building_tiers
322