itemPickerMenu.py (21446B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\shared\fittingScreen\itemPickerMenu.py 2 import carbonui.const as uiconst 3 from collections import defaultdict 4 import evetypes 5 import uthread2 6 import dynamicitemattributes 7 from carbonui.primitives.container import Container 8 from carbonui.primitives.containerAutoSize import ContainerAutoSize 9 from carbonui.primitives.sprite import Sprite 10 from carbonui.util.sortUtil import SortListOfTuples 11 from carbonui.control.buttonIcon import ButtonIcon 12 from carbonui.control.button import Button 13 from eve.client.script.ui.control.entries.generic import Generic 14 from eve.client.script.ui.control.entries.mouse_inside_scroll_entry import MouseInsideScrollEntry 15 from eve.client.script.ui.control.entries.util import GetFromClass 16 from eve.client.script.ui.control.eveIcon import Icon 17 from eve.client.script.ui.control.eveLabel import EveLabelMedium, EveLabelLarge 18 from eve.client.script.ui.control.eveScroll import Scroll 19 from eve.client.script.ui.control.labelEditable import LabelEditable 20 from eve.client.script.ui.shared.fitting.fittingUtil import EatSignalChangingErrors 21 from eve.client.script.ui.shared.fittingScreen.skillRequirements import GetMissingSkills_HighestLevelByTypeID 22 from eve.client.script.ui.util.uix import QtyPopup, GetTechLevelIcon 23 from localization import GetByLabel 24 import inventorycommon.const as invConst 25 from shipfitting.fittingWarningConst import WARNING_LEVEL_SKILL 26 from shipfitting.fittingWarnings import GetColorForLevel 27 from signals.signalUtil import ChangeSignalConnect 28 from utillib import KeyVal 29 from carbonui.uicore import uicore 30 from eveservices.menu import GetMenuService 31 from eve.common.lib import appConst as const 32 33 class ItemPickerBase(MouseInsideScrollEntry): 34 isDragObject = True 35 ENTRYHEIGHT = 32 36 ICONWIDTH = ENTRYHEIGHT - 4 37 NUM_CONT_WIDTH = 80 38 REMOVE_PADDING = 36 39 haveSkillsLabelPath = 'UI/Fitting/FittingWindow/HaveSkillsForItem' 40 dontHaveSkillsLabelPath = 'UI/Fitting/FittingWindow/MissingSkillsForItem' 41 42 def ApplyAttributes(self, attributes): 43 MouseInsideScrollEntry.ApplyAttributes(self, attributes) 44 self.fittingWarning = None 45 node = attributes.node 46 self.sr.node = node 47 self.updateNumThread = None 48 self.InitializeFuncs(node) 49 leftContWidth = self.NUM_CONT_WIDTH + self.ENTRYHEIGHT 50 leftCont = Container(name='leftCont', parent=self, align=uiconst.TOLEFT, width=leftContWidth) 51 self.numItemsCont = Container(name='numItemsCont', parent=leftCont, align=uiconst.TOLEFT, width=self.NUM_CONT_WIDTH, state=uiconst.UI_NORMAL) 52 self.restCont = Container(name='restCont', parent=self, align=uiconst.TOALL, left=10, width=self.REMOVE_PADDING) 53 self.iconCont = Container(name='iconCont', parent=leftCont, align=uiconst.TORIGHT, width=self.ENTRYHEIGHT) 54 self.typeIcon = Icon(parent=self.iconCont, pos=(0, 55 0, 56 self.ICONWIDTH, 57 self.ICONWIDTH), align=uiconst.CENTERRIGHT, idx=0, state=uiconst.UI_DISABLED) 58 self.techIcon = Sprite(name='techIcon', parent=self.iconCont, left=1, width=16, height=16, idx=0) 59 self.label = EveLabelMedium(text='', parent=self.restCont, left=0, top=0, state=uiconst.UI_DISABLED, color=None, maxLines=1, align=uiconst.CENTER) 60 self.AddRemoveBtn() 61 self.numItems = LabelEditable(name='numDrones_LabelEditable', parent=self.numItemsCont, align=uiconst.CENTERRIGHT, pos=(0, 1, 0, 0), text='10', hint=GetByLabel('UI/Fitting/FittingWindow/ClickToEdit'), configName='numDrones', maxLength=6, minLength=1, minValue=0, maxValue=1000000, defaultText='0', fontsize=18, onEditFieldChanged=self.OnEditFieldChanged, textEditFieldSwitchCallback=self.OnTextEditFieldChanges) 62 self.numItemsCont.OnClick = self.numItems.OnLabelClicked 63 self.numItemsCont.GetMenu = self.GetMenu 64 self.numItemsCont.isDragObject = True 65 self.numItemsCont.GetDragData = self.GetDragData 66 self.numItemsCont.hint = GetByLabel('UI/Fitting/FittingWindow/ClickToEdit') 67 self.xLabel = EveLabelLarge(text='x', parent=self.numItemsCont, left=self.numItems.left + 9, top=1, state=uiconst.UI_DISABLED, color=None, maxLines=1, align=uiconst.CENTERRIGHT) 68 69 def ConstructFittingWarning(self): 70 if self.fittingWarning and not self.fittingWarning.destroyed: 71 return 72 self.fittingWarning = Sprite(parent=self.iconCont, name='fittingWarning', state=uiconst.UI_DISABLED, align=uiconst.BOTTOMLEFT, pos=(4, 4, 16, 16), texturePath='res:/UI/Texture/classes/Fitting/slotWarningIcon.png', idx=0) 73 74 def Load(self, node): 75 self.typeID = node.typeID 76 self.label.text = node.label 77 self.typeIcon.SetSize(self.ENTRYHEIGHT - 4, self.ENTRYHEIGHT - 4) 78 self.typeIcon.LoadIconByTypeID(typeID=self.typeID, size=self.ICONWIDTH, ignoreSize=True, isCopy=getattr(node, 'isCopy', False)) 79 techSprite = GetTechLevelIcon(self.techIcon, 1, self.typeID) 80 techSprite.left = 4 81 self.numItems.SetValue(node.qty) 82 self.SetEditFieldRange() 83 84 def InitializeFuncs(self, node): 85 self.addToActiveFunc = node.addToActive 86 self.removeFromActiveFunc = node.removeFromActive 87 self.changeActive = node.changeActive 88 self.changeNumFunc = node.changeNum 89 90 def OnTextEditFieldChanges(self, *args): 91 if self.numItems.editField.display: 92 self.xLabel.display = False 93 else: 94 self.xLabel.display = True 95 96 def OnEditFieldChanged(self, *args): 97 LabelEditable.OnEditFieldChanged(self.numItems, *args) 98 if not self.updateNumThread: 99 self.updateNumThread = uthread2.call_after_wallclocktime_delay(self.UpdateNumbers_thread, 0.25) 100 101 def UpdateNumbers_thread(self): 102 self.updateNumThread = None 103 numWanted = self.numItems.GetValue() 104 if self.changeNumFunc: 105 self.changeNumFunc(self.sr.node.typeID, numWanted) 106 107 def SetEditFieldRange(self): 108 node = self.sr.node 109 self.numItems.editField.SetMinValue(node.intRange[0]) 110 self.numItems.editField.SetMaxValue(node.intRange[1]) 111 112 def AddRemoveBtn(self): 113 self.removeBtn = ButtonIcon(texturePath='res:/UI/Texture/Icons/73_16_210.png', pos=(2, 0, 16, 16), align=uiconst.CENTERRIGHT, parent=self, hint=GetByLabel('UI/Generic/RemoveItem'), idx=0, func=self.RemoveItem) 114 self.haveSkillIcon = Sprite(name='haveIcon', parent=self, pos=(20, 0, 16, 16), align=uiconst.CENTERRIGHT, state=uiconst.UI_NORMAL) 115 self.haveSkillIcon.display = False 116 117 def RemoveItem(self): 118 node = self.sr.node 119 if node.Get('removeFunc', None): 120 node.removeFunc(node) 121 122 def GetDragData(self): 123 typeID = self.sr.node.typeID 124 keyVal = KeyVal(__guid__='listentry.GenericMarketItem', typeID=typeID, label=evetypes.GetName(typeID), flagID=self.sr.node.flagID) 125 return [keyVal] 126 127 def GetHeight(self, *args): 128 node, width = args 129 node.height = ItemPickerBase.ENTRYHEIGHT 130 return node.height 131 132 @classmethod 133 def GetEntryWidth(cls, node): 134 label = node['label'] 135 textwidth, _ = EveLabelMedium.MeasureTextSize(label, maxLines=1) 136 w = cls.NUM_CONT_WIDTH + cls.ENTRYHEIGHT + textwidth + cls.REMOVE_PADDING + 40 137 return w 138 139 def GetMenu(self): 140 if self.sr.node.itemIDs: 141 itemID = list(self.sr.node.itemIDs)[0] 142 else: 143 itemID = None 144 return GetMenuService().GetMenuFromItemIDTypeID(itemID, self.sr.node.typeID, includeMarketDetails=True) 145 146 def SetFittingWarningColor(self, color): 147 if color is None: 148 if self.fittingWarning: 149 self.fittingWarning.display = False 150 else: 151 self.ConstructFittingWarning() 152 self.fittingWarning.display = True 153 self.fittingWarning.SetRGBA(*color) 154 155 156 class ItemPickerMenu(object): 157 entryClass = ItemPickerBase 158 emptyLabelPath = 'UI/Fitting/FittingWindow/NoCargoItemsSimulated' 159 160 def __init__(self, controller, menuParent): 161 self.scrollWidth = 100 162 self.fittingDogmaLocation = sm.GetService('clientDogmaIM').GetFittingDogmaLocation() 163 self.ghostFittingSvc = sm.GetService('ghostFittingSvc') 164 self.scroll = Scroll(parent=menuParent, align=uiconst.TOTOP, height=100, padding=4) 165 self.btnCont = ContainerAutoSize(parent=menuParent, align=uiconst.TOTOP, padTop=4, padBottom=4) 166 removeBtn = Button(parent=self.btnCont, label=GetByLabel('UI/Fitting/FittingWindow/RemoveAll'), func=self.RemoveAll, align=uiconst.CENTER) 167 self.scroll.GetEntryWidth = self.GetEntryWidth 168 169 def LoadItemScroll(self): 170 self.scroll.Clear() 171 sortedItemInfo = self.GetSortedItemsInfo() 172 activeItemsSet = set(self.GetActiveItems()) 173 extraData = self.GetExtraData() 174 extraDataFunc = self.GetExtraDataFuncs() 175 maxWidth = 100 176 scrollList = [] 177 for itemInfo in sortedItemInfo: 178 hasSkills = sm.GetService('skills').IsSkillRequirementMet(itemInfo.typeID) 179 qty = itemInfo.qty 180 maxExtraQty = GetMaxQty(self.fittingDogmaLocation, self.flagID, {itemInfo.typeID: 1}) or 0 181 intRange = (0, qty + maxExtraQty) 182 label = itemInfo.typeName 183 data = {'label': label, 184 'itemIDs': itemInfo.itemIDs, 185 'intRange': intRange, 186 'typeID': itemInfo.typeID, 187 'isDynamic': itemInfo.isDynamic, 188 'getIcon': True, 189 'OnClick': self.OnItemClicked, 190 'removeFunc': self.RemoveItemClicked, 191 'hasSkills': hasSkills, 192 'qty': qty, 193 'flagID': self.flagID} 194 data.update(extraData) 195 data.update(extraDataFunc) 196 extraDataForType = self.GetExtraDataForType(itemInfo.itemIDs, activeItemsSet, itemInfo.typeID) 197 data.update(extraDataForType) 198 width = self.entryClass.GetEntryWidth(data) 199 maxWidth = max(maxWidth, width) 200 entry = GetFromClass(self.entryClass, data) 201 scrollList.append(entry) 202 203 if scrollList: 204 entry = self.GetHeaderEntry(activeItemsSet) 205 if entry: 206 scrollList.insert(0, entry) 207 self.scrollWidth = maxWidth 208 self.btnCont.display = True 209 else: 210 scrollList = self.GetNoItemEntryAndSetWidth() 211 if not self.DoShowBtnWhenScollIsEmpty(): 212 self.btnCont.display = False 213 self.scroll.AddNodes(0, scrollList) 214 self.UpdateScrollSize() 215 216 def RefrehsScroll(self): 217 self.scroll.RefreshNodes() 218 219 def DoShowBtnWhenScollIsEmpty(self): 220 return False 221 222 def UpdateNodes(self): 223 pass 224 225 def IsItemChecked(self, itemID, activeItemsCopy): 226 return True 227 228 def GetActiveItems(self): 229 return {} 230 231 def GetItems(self): 232 return {} 233 234 def GetTotalCanActivate(self, numActive): 235 return 0 236 237 def OnItemClicked(self, *args): 238 pass 239 240 def UpdateScrollSize(self): 241 contentHeight = self.scroll.GetContentHeight() + 2 242 self.scroll.height = min(contentHeight, 400) 243 244 def GetEntryWidth(self, *args): 245 return self.scrollWidth 246 247 def RemoveItemClicked(self, node): 248 self.RemoveItem(node) 249 self.scroll.RemoveNodes([node]) 250 if not self.scroll.GetNodes(): 251 scrollList = self.GetNoItemEntryAndSetWidth() 252 if not self.DoShowBtnWhenScollIsEmpty(): 253 self.btnCont.display = False 254 self.scroll.AddNodes(0, scrollList) 255 self.UpdateScrollSize() 256 257 def RemoveItem(self, node): 258 itemIDs = node.itemIDs 259 if not itemIDs: 260 return 261 itemID = list(itemIDs)[0] 262 self.ghostFittingSvc.UnfitModule(itemID) 263 264 def GetNoItemEntryAndSetWidth(self): 265 data = {'label': GetByLabel(self.emptyLabelPath)} 266 data.update(self.GetNoItemDropFunc()) 267 scrollList = [GetFromClass(NoItemEntry, data)] 268 width = NoItemEntry.GetEntryWidth(NoItemEntry, data) 269 self.scrollWidth = width 270 return scrollList 271 272 def GetNoItemDropFunc(self): 273 return {} 274 275 def GetSortedItemsInfo(self): 276 itemsToSort = self.GetItems() 277 unsortedEntries = {} 278 dynamicItemSvc = sm.GetService('dynamicItemSvc') 279 for eachItem in itemsToSort.itervalues(): 280 identifier = None 281 if dynamicItemSvc.IsDynamicItem(eachItem.typeID): 282 identifier = eachItem.itemID 283 itemKey = (eachItem.typeID, identifier) 284 itemEntryData = unsortedEntries.get(itemKey) 285 if not itemEntryData: 286 itemEntryData = ItemPickerItemEntryData(typeID=eachItem.typeID, typeName=evetypes.GetName(eachItem.typeID), isDynamic=identifier is not None, itemIDs=set(), qty=0) 287 unsortedEntries[itemKey] = itemEntryData 288 itemEntryData.itemIDs.add(eachItem.itemID) 289 itemEntryData.qty += getattr(eachItem.invItem, 'quantity', None) or getattr(eachItem, 'stacksize', 1) 290 291 return sorted(unsortedEntries.values(), key=lambda data: data.typeName) 292 293 def RemoveAll(self, *args): 294 for eachNode in self.scroll.GetNodes(): 295 self.RemoveItem(eachNode) 296 297 self.ghostFittingSvc.SendOnStatsUpdatedEvent() 298 self.LoadItemScroll() 299 300 def GetExtraData(self): 301 return {} 302 303 def GetExtraDataFuncs(self): 304 return {} 305 306 def GetExtraDataForType(self, itemIDs, itemIdSet, typeID): 307 return {} 308 309 def GetHeaderEntry(self, extraInfo): 310 return None 311 312 def GetAllTypeIDsInScroll(self): 313 allTypeIDsInScroll = filter(None, {n.typeID for n in self.scroll.GetNodes()}) 314 return allTypeIDsInScroll 315 316 def HiliteProblematicEntries(self, warningSlotDict): 317 pass 318 319 def UpdateProblematicEntries(self, warningLevel): 320 typeIDsToUpdate = self.GetAllTypeIDsInScroll() 321 if warningLevel and warningLevel == WARNING_LEVEL_SKILL: 322 _, typeIDsToUpdate = GetMissingSkills_HighestLevelByTypeID(typeIDsToUpdate) 323 color = GetColorForLevel(warningLevel) 324 for eachNode in self.scroll.GetNodes(): 325 if not eachNode.typeID: 326 continue 327 elif eachNode.typeID not in typeIDsToUpdate: 328 eachNode.warningLevel = None 329 continue 330 eachNode.warningLevel = warningLevel 331 if eachNode.panel and getattr(eachNode.panel, 'SetFittingWarningColor', None): 332 eachNode.panel.SetFittingWarningColor(color) 333 334 335 class ItemPickerItemEntryData(object): 336 337 def __init__(self, typeID, itemIDs, isDynamic, typeName, qty): 338 self.typeID = typeID 339 self.itemIDs = itemIDs 340 self.isDynamic = isDynamic 341 self.typeName = typeName 342 self.qty = qty 343 344 345 class BaseHoldItemPickerMenu(ItemPickerMenu): 346 entryClass = ItemPickerBase 347 flagID = const.flagCargo 348 349 def GetItems(self): 350 items = self.fittingDogmaLocation.GetHoldItems(self.flagID) 351 return items 352 353 def RemoveAll(self, *args): 354 self.fittingDogmaLocation.RemoveAllItemsFromHold(self.flagID) 355 self.ghostFittingSvc.SendOnStatsUpdatedEvent() 356 self.LoadItemScroll() 357 358 def GetExtraDataFuncs(self): 359 return {'changeNum': self.ChangeNumItems} 360 361 def ChangeNumItems(self, typeID, numWanted): 362 self.ghostFittingSvc.ModifyHoldItemStackSize(typeID, numWanted, self.flagID) 363 self.UpdateNodes() 364 365 def HiliteProblematicEntries(self, warningSlotDict): 366 warningLevel = warningSlotDict.get(self.flagID, None) 367 self.UpdateProblematicEntries(warningLevel) 368 369 def UpdateNodes(self): 370 qtyByTypeID = {itemInfo.typeID:itemInfo.qty for itemInfo in self.GetSortedItemsInfo()} 371 for eachNode in self.scroll.GetNodes(): 372 typeID = eachNode.typeID 373 if not typeID: 374 continue 375 qty = qtyByTypeID.get(typeID, 0) 376 maxExtraQty = GetMaxQty(self.fittingDogmaLocation, eachNode.flagID, {typeID: 1}) or 0 377 intRange = (0, qty + maxExtraQty) 378 eachNode.intRange = intRange 379 380 for eachNode in self.scroll.GetNodes(): 381 if eachNode.panel: 382 eachNode.panel.SetEditFieldRange() 383 384 385 class CargoItemPickerMenu(BaseHoldItemPickerMenu): 386 entryClass = ItemPickerBase 387 flagID = const.flagCargo 388 389 def __init__(self, controller, menuParent): 390 BaseHoldItemPickerMenu.__init__(self, controller, menuParent) 391 self.controller = controller 392 self.ChangeSignalConnection(connect=True) 393 394 def ChangeSignalConnection(self, connect = True): 395 signalAndCallback = [(self.controller.on_warning_display_changed, self.HiliteProblematicEntries)] 396 ChangeSignalConnect(signalAndCallback, connect) 397 398 def DisconnectController(self): 399 with EatSignalChangingErrors(errorMsg='CargoItemPickerMenu'): 400 self.ChangeSignalConnection(connect=False) 401 self.controller = False 402 403 404 def GetModifiedQtyDict(fittingDogmaLocation, qtyByTypeIDs, flagID): 405 if uicore.uilib.Key(uiconst.VK_SHIFT): 406 qty = FindQty(fittingDogmaLocation, qtyByTypeIDs, flagID) 407 if qty is None: 408 return {} 409 for eachTypeID in qtyByTypeIDs: 410 qtyByTypeIDs[eachTypeID] = qty 411 412 return qtyByTypeIDs 413 414 415 def FindQty(fittingDogmaLocation, qtyByTypeIDs, flagID): 416 maxQty = GetMaxQty(fittingDogmaLocation, flagID, qtyByTypeIDs) 417 if not maxQty: 418 return 419 ret = QtyPopup(maxvalue=maxQty, minvalue=1) 420 if ret is None: 421 qty = None 422 else: 423 qty = ret['qty'] 424 return qty 425 426 427 def GetMaxQty(fittingDogmaLocation, flagID, qtyByTypeIDs): 428 shipID = fittingDogmaLocation.GetCurrentShipID() 429 capacityInfo = fittingDogmaLocation.GetCapacity(shipID, None, flagID) 430 volumeLeft = capacityInfo.capacity - capacityInfo.used 431 maxQtyForType = 0 432 allTypeVol = 0 433 for typeID in qtyByTypeIDs: 434 if isinstance(typeID, tuple): 435 typeID = typeID[0] 436 typeVolume = float(evetypes.GetVolume(typeID)) 437 allTypeVol += typeVolume 438 maxQtyForType = max(maxQtyForType, int(volumeLeft / typeVolume)) 439 440 maxQty = 0 441 if allTypeVol: 442 maxQty = volumeLeft / allTypeVol 443 if not maxQtyForType: 444 return 445 return int(maxQty) 446 447 448 def GetDronesInDamgeAmountOrder(fittingDogmaLocation, droneIDs): 449 damageTypeAttibutes = [const.attributeEmDamage, 450 const.attributeThermalDamage, 451 const.attributeKineticDamage, 452 const.attributeExplosiveDamage] 453 topDamgeOrder = [] 454 GAV = fittingDogmaLocation.GetAttributeValue 455 for eachDroneID in droneIDs: 456 damageModifier = GAV(eachDroneID, const.attributeDamageMultiplier) 457 dmg = sum({GAV(eachDroneID, x) for x in damageTypeAttibutes}) 458 totalDmg = damageModifier * dmg 459 topDamgeOrder.append((totalDmg, eachDroneID)) 460 461 topDamgeOrder = SortListOfTuples(topDamgeOrder, reverse=True) 462 return topDamgeOrder 463 464 465 class NoItemEntry(Generic): 466 __guid__ = 'NoItemEntry' 467 default_showHilite = False 468 ENTRYHEIGHT = 28 469 470 def GetHeight(self, *args): 471 return NoItemEntry.ENTRYHEIGHT 472 473 def GetEntryWidth(cls, node): 474 label = node['label'] 475 textwidth, textheight = EveLabelMedium.MeasureTextSize(label, maxLines=1) 476 return textwidth + 20 477 478 def OnDropData(self, dragObj, nodes): 479 onDroppedInListFunc = getattr(self.sr.node, 'onDroppedInListFunc', None) 480 if onDroppedInListFunc: 481 onDroppedInListFunc(nodes) 482 483 484 VALID_CHARGE_CATEGORIES = [invConst.categoryCharge, invConst.categoryImplant] 485 VALID_CHARGE_GROUPS = [invConst.groupIceProduct] 486 VALID_MODULE_CATEGORIES = [invConst.categoryModule, invConst.categorySubSystem] 487 488 def IsValidForHold(typeID, flagID): 489 categoryID = evetypes.GetCategoryID(typeID) 490 if flagID == const.flagCargo: 491 if categoryID in VALID_CHARGE_CATEGORIES: 492 return True 493 if categoryID in VALID_MODULE_CATEGORIES: 494 return True 495 groupID = evetypes.GetGroupID(typeID) 496 if groupID in VALID_CHARGE_GROUPS: 497 return True 498 if typeID in evetypes.GetTypeIDsByListID(evetypes.TYPE_LIST_FILAMENTS): 499 return True 500 if typeID in evetypes.GetTypeIDsByListID(evetypes.TYPE_LIST_OTHER_CARGO_HOLD_TYPES): 501 return True 502 if flagID == const.flagFighterBay: 503 if categoryID == const.categoryFighter: 504 return True 505 return False 506 507 508 allowedCategories = [const.categoryAccessories, 509 const.categoryAncientRelic, 510 const.categoryApparel, 511 const.categoryAsteroid, 512 const.categoryBlueprint, 513 const.categoryCharge, 514 const.categoryCommodity, 515 const.categoryDecryptors, 516 const.categoryDeployable, 517 const.categoryDrone, 518 const.categoryImplant, 519 const.categoryMaterial, 520 const.categoryModule, 521 const.categoryPlanetaryCommodities, 522 const.categoryPlanetaryInteraction, 523 const.categoryPlanetaryResources, 524 const.categoryShip, 525 const.categoryShipSkin, 526 const.categorySkill, 527 const.categoryInfrastructureUpgrade, 528 const.categoryStructureModule, 529 const.categorySubSystem, 530 const.categorySpecialEditionAssets, 531 const.categoryFighter] 532 533 def IsAllowedToAddAtAll(typeID): 534 if dynamicitemattributes.IsDynamicType(typeID): 535 return False 536 categoryID = evetypes.GetCategoryID(typeID) 537 return categoryID in allowedCategories