def parse(self, dbr, dbr_file, result): # Skip formula without artifacts if self.ARTIFACT not in dbr: raise StopIteration artifact = DBRParser.parse(dbr[self.ARTIFACT]) # Update the result with the artifact: result['tag'] = artifact['tag'] result['name'] = artifact['name'] result['classification'] = artifact['classification'] if self.BITMAP in dbr: result['bitmap'] = dbr[self.BITMAP] # Grab the reagents (ingredients): for reagent_key in ['reagent1', 'reagent2', 'reagent3']: # For some reason reagent DBRs are of type array, so grab [0]: reagent = DBRParser.parse(dbr[reagent_key + 'BaseName'][0]) # Add the reagent (relic, scroll or artifact) result[reagent_key] = reagent['tag'] # Add the potential completion bonuses bonuses = DBRParser.parse(dbr['artifactBonusTableName']) result['bonus'] = bonuses.get('table', []) # Last but not least, pop the 'properties' from this result, since # formula don't have the properties themselves, but their respective # artifacts do. result.pop('properties')
def parse(self, dbr, dbr_file, result): """ Parse a possible skill bonus for a pet. These bonuses are things like: - 15 Vitality Damage - +5% Vitality Damage """ if self.NAME in dbr: # Parse the pet bonus and add it: pet_bonus = DBRParser.parse(dbr[self.NAME]) properties = ( # If a tiered property set is found, return the first entry pet_bonus['properties'][0] if isinstance(pet_bonus['properties'], list) # Otherwise just return the list else pet_bonus['properties']) # Don't allow nested petBonus properties # One example of this is the Spear of Nemetona if 'petBonus' in properties: properties.pop('petBonus') # Set the properties of the bonus as the value for this field: result['properties']['petBonus'] = properties
def parse(self, dbr, dbr_file, result): """ Parse a possible skill bonus for a pet. These bonuses are things like: - 15 Vitality Damage - +5% Vitality Damage """ if self.NAME in dbr: # Parse the pet bonus and add it: pet_bonus = DBRParser.parse(dbr[self.NAME]) properties = ( # If a tiered property set is found, return the first entry pet_bonus['properties'][0] if isinstance( pet_bonus['properties'], list) # Otherwise just return the list else pet_bonus['properties']) # Don't allow nested petBonus properties # One example of this is the Spear of Nemetona if 'petBonus' in properties: properties.pop('petBonus') # Set the properties of the bonus as the value for this field: result['properties']['petBonus'] = properties
def parse(self, dbr, dbr_file, result): """ Parse skill augments, mastery augments, and item granted skills. """ self._parse_skill_grant(dbr, result) # Parse skills that are augmented: for name, level in self.SKILL_AUGMENTS.items(): # Skip skills without both the name and level set: if name not in dbr or level not in dbr: continue skill = DBRParser.parse(dbr[name]) # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) level = dbr[level] # Skill format is either ItemSkillIncrement or ItemMasteryIncrement skill_format = (self.TXT_SKILL_INC if 'Mastery' not in skill['name'] else self.TXT_MASTERY_INC) result['properties'][name] = { 'tag': skill_tag, 'name': texts.get(skill_format).format(level, skill['name']), } # Parse augment to all skills: if self.AUGMENT_ALL in dbr: level = dbr[self.AUGMENT_ALL] result['properties'][self.AUGMENT_ALL] = ( texts.get(self.TXT_ALL_INC).format(level))
def parse_sets(): """ Parse the Titan Quest equipment sets. The equipment sets are indexed and their properties are the set bonuses you receive for wearing multiple set pieces at once. """ timer = time.clock() files = [] for resource in resources.SETS: set_files = resources.DB / resource files.extend(glob.glob(str(set_files), recursive=True)) sets = {} for dbr in files: parsed = parse(dbr) try: # Add the set by its tag to the dictionary of sets: sets[parsed['tag']] = parsed except KeyError: # Skip sets with no tag: continue # Log the timer: logging.info(f'Parsed sets in {time.clock() - timer} seconds.') return sets
def _parse_skill_grant(self, dbr, result): """ Parse a granted skill. """ # Skip files without both granted skill name and level: if self.SKILL_NAME not in dbr or self.SKILL_LEVEL not in dbr: return level = dbr[self.SKILL_LEVEL] skill = DBRParser.parse(dbr[self.SKILL_NAME]) if 'name' not in skill: return # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Now add the granted skill to the item: result['properties'][self.SKILL_NAME] = { 'tag': skill_tag, 'level': level, } if self.CONTROLLER not in dbr: return # Grab the auto controller to see if this skill is activated: controller = DBRParser.read(dbr[self.CONTROLLER]) # The property 'triggerType' can be converted to a useful text suffix # like 'Activated on attack': result['properties'][self.SKILL_NAME].update( {'trigger': texts.get(self.TRIGGERS[controller['triggerType']])})
def parse(self, dbr, dbr_file, result): if 'level' in result['references']: # This camelCased variable is required for the level equations. # Grab the level from the references which has been passed: averagePlayerLevel = result['references']['level'] # noqa parentLevel = result['references']['level'] # noqa # Calculate the minimum and maximum levels: try: min_level = numexpr.evaluate(dbr['minItemLevelEquation']).item() max_level = numexpr.evaluate(dbr['maxItemLevelEquation']).item() target_level = numexpr.evaluate(dbr['targetLevelEquation']).item() except KeyError: # Log the missing variable: logging.info(f'Missing parentLevel in {dbr_file}') return # Grab the slope and defaultWeight, to use for adjusting values later: slope = dbr['bellSlope'] weight = dbr['defaultWeight'] # Store the drop and their adjusted weights in this dictionary: drops = {} for index, loot_file in enumerate(dbr.get('itemNames', [])): # Grab the item and its chance try: item = DBRParser.parse(loot_file) except InvalidItemError as e: logging.debug( f'Invalid loot file {loot_file} in {dbr_file}. {e}') continue if 'tag' not in item: logging.debug(f'No tag for {loot_file} in {dbr_file}') continue level = item['itemLevel'] # Skip all items outside the range if level is None or level < min_level or level > max_level: continue # Next compare the item's level to the target level target = int(level - target_level) # Grab the adjustment from the slope (or the last one) adjustment = slope[target] if len(slope) > target else slope[-1] # The adjusted weight is the default multiplied by the adjustment: drops[item['tag']] = weight * adjustment # The sum of all weights can now be calculated summed = sum(v for v in drops.values()) # Store the chance of this item by its tag: result['loot_table'] = { tag: float('{0:.5f}'.format(item_weight / summed)) for tag, item_weight in drops.items() }
def parse(self, dbr, dbr_file, result): items = {} # Add up all the loot weights: summed = sum(v for k, v in dbr.items() if k.startswith('lootWeight')) # Run through all the loot chances and parse them: for i in range(1, 31): weight = dbr.get(f'lootWeight{i}', 0) # Skip items with no chance: if not weight: continue try: # Grab the item and its chance item = DBRParser.parse(dbr[f'lootName{i}']) # Store the chance of this item by its tag: items[item['tag']] = float('{0:.5f}'.format(weight / summed)) except KeyError: # Skip items that have no tag: continue result['loot_table'] = items
def parse_sets(): """ Parse the Titan Quest equipment sets. The equipment sets are indexed and their properties are the set bonuses you receive for wearing multiple set pieces at once. """ start_time = time.time() files = [] for resource in resources.SETS: set_files = resources.DB / resource files.extend(glob.glob(str(set_files), recursive=True)) sets = {} for dbr in files: try: parsed = parse(dbr) except InvalidItemError as e: exception_messages = exception_messages_with_causes(e) logging.debug(f"Ignoring item in {dbr}. {exception_messages}") continue try: # Add the set by its tag to the dictionary of sets: sets[parsed['tag']] = parsed except KeyError: # Skip sets with no tag: continue # Log the timer: logging.info(f'Parsed sets in {time.time() - start_time:.2f} seconds.') return sets
def _parse_skill_grant(self, dbr, result): """ Parse a granted skill. """ # Skip files without both granted skill name and level: if self.SKILL_NAME not in dbr or self.SKILL_LEVEL not in dbr: return level = dbr[self.SKILL_LEVEL] skill = DBRParser.parse(dbr[self.SKILL_NAME]) if 'name' not in skill: return # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Now add the granted skill to the item: result['properties'][self.SKILL_NAME] = { 'tag': skill_tag, 'level': level, } if self.CONTROLLER not in dbr: return # Grab the auto controller to see if this skill is activated: controller = DBRParser.read(dbr[self.CONTROLLER]) # The property 'triggerType' can be converted to a useful text suffix # like 'Activated on attack': result['properties'][self.SKILL_NAME].update({ 'trigger': texts.get(self.TRIGGERS[controller['triggerType']]) })
def parse_equipment(): """ Parse all wearable Titan Quest equipment. The wearable equipment is indexed and sorted by equipment type. These categories are defined by the Class property of each piece of equipment which is mapped to the 'category' key in the parsed result. """ timer = time.clock() files = [] for resource in resources.EQUIPMENT: equipment_files = resources.DB / resource # Exclude all files in 'old' and 'default' files.extend([ equipment_file for equipment_file in glob.glob(str(equipment_files), recursive=True) if not ( '\\old' in equipment_file or '\\default' in equipment_file or # Rhodian and Electrum sling don't drop: '\\1hranged\\u_e_02.dbr' in equipment_file or '\\1hranged\\u_n_05.dbr' in equipment_file ) ]) items = {} for dbr in files: parsed = parse(dbr) try: # Organize the equipment based on the category category = parsed.pop('category') # Skip items without rarities if 'classification' not in parsed: continue # Save the bitmap and remove the bitmap key images.save_bitmap(parsed, category, 'output/graphics/') except KeyError: # Skip equipment that couldn't be parsed: continue # Pop off the properties key off any item without properties: if 'properties' in parsed and not parsed['properties']: parsed.pop('properties') # Now save the parsed item in the category: if category and category in items: items[category].append(parsed) elif category: items[category] = [parsed] # Log the timer: logging.info(f'Parsed equipment in {time.clock() - timer} seconds.') return items
def parse(self, dbr, dbr_file, result): """ Parse the referenced pet skill, and pass that back as this result. """ if self.PET_SKILL in dbr: # Now set our result as the result of the pet skill being parsed: result.update(DBRParser.parse(dbr[self.PET_SKILL]))
def parse(self, dbr, dbr_file, result): """ Parse the referenced buff skill, and pass that back as this result. """ if self.BUFF in dbr: # Now set our result as the result of the buff being parsed: result.update(DBRParser.parse(dbr[self.BUFF]))
def parse_equipment(): """ Parse all wearable Titan Quest equipment. The wearable equipment is indexed and sorted by equipment type. These categories are defined by the Class property of each piece of equipment which is mapped to the 'category' key in the parsed result. """ timer = time.clock() files = [] for resource in resources.EQUIPMENT: equipment_files = resources.DB / resource # Exclude all files in 'old' and 'default' files.extend([ equipment_file for equipment_file in glob.glob(str(equipment_files), recursive=True) if not ( '\\old' in equipment_file or '\\default' in equipment_file or # Rhodian and Electrum sling don't drop: '\\1hranged\\u_e_02.dbr' in equipment_file or '\\1hranged\\u_n_05.dbr' in equipment_file) ]) items = {} for dbr in files: parsed = parse(dbr) try: # Organize the equipment based on the category category = parsed.pop('category') # Skip items without rarities if 'classification' not in parsed: continue # Save the bitmap and remove the bitmap key images.save_bitmap(parsed, category, 'output/graphics/') except KeyError: # Skip equipment that couldn't be parsed: continue # Pop off the properties key off any item without properties: if 'properties' in parsed and not parsed['properties']: parsed.pop('properties') # Now save the parsed item in the category: if category and category in items: items[category].append(parsed) elif category: items[category] = [parsed] # Log the timer: logging.info(f'Parsed equipment in {time.clock() - timer} seconds.') return items
def parse_difficulty(self, dbr, dbr_file): """ Parse a difficulty of equipable loot. """ result = {} # Parse all equipable loot: for equipment in self.EQUIPMENT_SLOTS: equip_key = f'chanceToEquip{equipment}' equip_chance = dbr.get(equip_key, 0) # Skip equipment that has 0 chance to be equiped if not equip_chance: continue equip_key = f'{equip_key}Item' # Iterate over all the possibilities and sum up the weights: summed = sum(v for k, v in dbr.items() if k.startswith(equip_key)) for i in range(1, 7): weight = dbr.get(f'{equip_key}{i}', 0) # Skip slots that have 0 chance if not weight: continue chance = float('{0:.5f}'.format(weight / summed)) # Grab the loot table holding the equipment list: loot_key = f'loot{equipment}Item{i}' loot_file = dbr.get(loot_key) if not loot_file or not loot_file.is_file(): logging.debug(f'No {loot_key} in {dbr_file}') continue loot = DBRParser.parse(loot_file, {'level': dbr['charLevel']}) if 'tag' in loot: # Add a single item that was found: self.add_items( result, {loot['tag']: chance * equip_chance}) elif 'loot_table' in loot: # Add all the items (and multiply their chances) items = dict( (k, v * chance * equip_chance) for k, v in loot['loot_table'].items()) self.add_items(result, items) # Convert all item chances to 4 point precision: result = dict( (k, float('{0:.4f}'.format(v))) for k, v in result.items()) return result
def parse(self, dbr, dbr_file, result): if 'level' in result['references']: # This camelCased variable is required for the level equations. # Grab the level from the references which has been passed: averagePlayerLevel = result['references']['level'] # noqa parentLevel = result['references']['level'] # noqa # Calculate the minimum and maximum levels: try: min_level = numexpr.evaluate(dbr['minItemLevelEquation']).item() max_level = numexpr.evaluate(dbr['maxItemLevelEquation']).item() target_level = numexpr.evaluate(dbr['targetLevelEquation']).item() except KeyError: # Log the missing variable: logging.info(f'Missing parentLevel in {dbr_file}') return # Grab the slope and defaultWeight, to use for adjusting values later: slope = dbr['bellSlope'] weight = dbr['defaultWeight'] # Store the drop and their adjusted weights in this dictionary: drops = {} for index, loot_file in enumerate(dbr.get('itemNames', [])): # Grab the item and its chance item = DBRParser.parse(loot_file) if 'tag' not in item: logging.debug(f'No tag for {loot_file} in {dbr_file}') continue level = item['itemLevel'] # Skip all items outside the range if level < min_level or level > max_level: continue # Next compare the item's level to the target level target = int(level - target_level) # Grab the adjustment from the slope (or the last one) adjustment = slope[target] if len(slope) > target else slope[-1] # The adjusted weight is the default multiplied by the adjustment: drops[item['tag']] = weight * adjustment # The sum of all weights can now be calculated summed = sum(v for v in drops.values()) # Store the chance of this item by its tag: result['loot_table'] = { tag: float('{0:.5f}'.format(item_weight / summed)) for tag, item_weight in drops.items() }
def parse(self, dbr, dbr_file, result): tag = dbr.get(self.NAME, None) if not tag or texts.get(tag) == tag: logging.warning(f'No tag or name for set found in {dbr_file}.') raise StopIteration result.update({ # Prepare the list of set items 'items': [], 'name': texts.get(tag), 'tag': tag, }) # Add the set members: for set_member_path in dbr['setMembers']: # Parse the set member: set_member = DBRParser.parse(set_member_path) # Add the tag to the items list: result['items'].append(set_member['tag']) # The number of set bonuses is equal to the number of set items minus 1 bonus_number = len(result['items']) - 1 # Because this parser has the lowest priority, all properties will # already have been parsed, so they can now be reconstructed to match # the set bonuses. Begin by initializing the properties for each set # bonus tier to an empty dict: properties = [{} for i in range(bonus_number)] # Insert the existing properties by adding them to the correct tier: for field, values in result['properties'].items(): if not isinstance(values, list): properties[bonus_number - 1][field] = values # Don't parse any further continue # The starting tier is determined by the highest tier starting_index = bonus_number - len(values) # Now just iterate and add the properties to each tier: for index, value in enumerate(values): properties[starting_index + index][field] = value # Now set the tiered set bonuses: result['properties'] = properties # Pop off the first element of the properties, if it's empty: if len(result['properties']) > 1: if not result['properties'][0]: result['properties'].pop(0)
def parse(self, dbr, dbr_file, result): if 'tables' not in dbr: logging.debug(f'No table found in {dbr_file}') raise StopIteration # Parse the references 'tables' file and set the result: loot = DBRParser.parse( dbr['tables'][0], # Always pass along any references that were set: result['references'], ) result['loot_table'] = loot['loot_table']
def parse(self, dbr, dbr_file, result): """ Parse the skill that spawns a pet. """ # Only set time to live it's set (otherwise it's infinite) ttl_list = dbr.get(self.TTL, None) # Parse all the summons and set them as a list: result['summons'] = [] for index, spawn_file in enumerate(dbr['spawnObjects']): spawn = DBRParser.parse(spawn_file) # Keep track of the original properties this summon had: original_properties = {} if 'properties' in spawn: if isinstance(spawn['properties'], list): original_properties = spawn['properties'][0].copy() else: original_properties = spawn['properties'].copy() # We need the raw values from the spawn DBR for hp/mp spawn['properties'] = {} spawn_dbr = DBRParser.read(spawn_file) if 'characterLife' in spawn_dbr: hp_list = spawn_dbr['characterLife'] hp = (hp_list[index] if index < len(hp_list) else hp_list[len(hp_list) - 1]) TQDBParser.insert_value('characterLife', texts.get('LifeText').format(hp), spawn) if 'characterMana' in spawn_dbr: mp_list = spawn_dbr['characterMana'] mp = (mp_list[index] if index < len(mp_list) else mp_list[len(mp_list) - 1]) TQDBParser.insert_value('characterMana', texts.get('ManaText').format(mp), spawn) if ttl_list: ttl = (ttl_list[index] if index < len(ttl_list) else ttl_list[len(ttl_list) - 1]) TQDBParser.insert_value(self.TTL, texts.get(self.TTL).format(ttl), spawn) # Iterate over the original properties and add some whitelisted # properties to the final result: for key, value in original_properties.items(): if key.startswith('character'): continue spawn['properties'][key] = value result['summons'].append(spawn)
def parse(self, dbr, dbr_file, result): items = {} # Add up all the loot weights: summed = sum(v for k, v in dbr.items() if k.startswith('lootWeight')) # Run through all the loot entries and parse them: for i in range(1, 31): weight = dbr.get(f'lootWeight{i}', 0) # Skip items with no chance: if not weight: continue chance = float('{0:.5f}'.format(weight / summed)) try: # Try to parse the referenced loot file loot_file = dbr[f'lootName{i}'] except KeyError: logging.debug(f'No lootName{i} not found in {dbr_file}.') continue # Parse the loot file try: loot = DBRParser.parse( loot_file, # Always pass along any references that were set: result['references'], ) except InvalidItemError as e: logging.debug( f"Invalid lootName{i} in {loot_file} referenced by {dbr_file}." ) continue # e.g. xpack2\quests\rewards\loottables\generic_rareweapon_n.dbr # The entry lootName15 has two entries separated by ';' if 'loot_table' not in loot: logging.debug(f'Invalid lootName{i} in {dbr_file}.') continue # Loot entries will be in 'table', add those: for k, v in loot['loot_table'].items(): if k in items: items[k] += (v * chance) else: items[k] = (v * chance) # Add the parsed loot table result['loot_table'] = items
def parse(self, dbr, dbr_file, result): # Skip formula without artifacts if self.ARTIFACT not in dbr: raise InvalidItemError(f"Artifact {dbr_file} has no {self.ARTIFACT}.") artifact = DBRParser.parse(dbr[self.ARTIFACT]) # Update the result with the artifact: result['tag'] = artifact['tag'] result['name'] = artifact['name'] result['classification'] = artifact['classification'] if self.BITMAP in dbr: result['bitmap'] = dbr[self.BITMAP] # Grab the reagents (ingredients): for reagent_key in ['reagent1', 'reagent2', 'reagent3']: # For some reason reagent DBRs are of type array, so grab [0]: reagent = DBRParser.parse(dbr[reagent_key + 'BaseName'][0]) # Add the reagent (relic, scroll or artifact) result[reagent_key] = reagent['tag'] # Add the potential completion bonuses bonuses = {} try: bonuses = DBRParser.parse(dbr['artifactBonusTableName']) except InvalidItemError as e: logging.debug("Could not parse artifact completion bonus " f"information for {result['name']} in {dbr_file}. " f"{e}") result['bonus'] = bonuses.get('table', []) # Last but not least, pop the 'properties' from this result, since # formula don't have the properties themselves, but their respective # artifacts do. result.pop('properties')
def parse_creatures(): """ Parse all creatures (bosses and heroes) in Titan Quest. Parsing the bosses and heroes is mostly about parsing their loot tables to create an index of what they can drop. This index will work two ways, the first being a complete list of items that the monster can drop and the reverse being added to each individual item's loot table so it can be sorted. """ start_time = time.time() files = [] for resource in resources.CREATURES: boss_files = resources.DB / resource files.extend(glob.glob(str(boss_files), recursive=True)) logging.info(f"Found {len(files)} creature files.") creatures = {} for dbr in files: try: logging.debug(f"Attempting to parse creature in {dbr}.") parsed = parse(dbr) except InvalidItemError as e: logging.debug(f"Ignoring creature in {dbr}. {e}") continue try: # Don't include common monsters # XXX - Should 'Champion' be added? # Should this be moved to MonsterParser to save work? The equipment # parser does that. if parsed['classification'] not in ['Quest', 'Hero', 'Boss']: continue # Store the monster by its tag: creatures[parsed['tag']] = parsed except KeyError: # Skip creatures without tags logging.debug(f"Ignoring creature in {dbr}. No classification " "present.") continue # Log the timer: logging.info( f'Parsed creatures in {time.time() - start_time:.2f} seconds.') return creatures
def parse(self, dbr, dbr_file, result): """ Parse all the abilities a monster has. """ # Initialize the abilities (to be indexed per level) abilities = [] # Parse all the normal skills (17 max): for i in range(1, 18): nameTag = f'skillName{i}' levelTag = f'skillLevel{i}' # Skip unset skills or skills that are to be ignored: if (nameTag not in dbr or levelTag not in dbr or str(dbr[nameTag]).lower() in self.IGNORE_SKILLS): continue try: skill = DBRParser.parse(dbr[nameTag]) except InvalidItemError as e: logging.debug( f"Skipping creature skill {nameTag} in {dbr_file} because it's invalid. {e}" ) continue if not skill['properties']: continue # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Iterate over the difficulties: for difficulty in range(3): itr = TQDBParser.extract_values(dbr, 'skill', difficulty) # If a level is set to 0 for a difficulty, it won't be in the # extracted result, so use the KeyError safe 'get' method: level = itr.get(levelTag, 0) if not level: continue if len(abilities) - 1 < difficulty: # Create missing tiers: abilities += ([{}] * (difficulty - len(abilities) + 1)) abilities[difficulty][skill_tag] = level result['abilities'] = abilities
def parse(self, dbr, dbr_file, result): """ Parse the scroll. """ # The new Potion XP items are also considered "scrolls": if 'potion' in str(dbr_file): return result # Use the file name to determine the difficulty: file_name = os.path.basename(dbr_file).split('_')[0][1:] # Strip all but digits from the string, then cast to int: difficulty = self.DIFFICULTIES_LIST[int( ''.join(filter(lambda x: x.isdigit(), file_name))) - 1] result.update({ 'tag': dbr['description'], 'name': texts.get(dbr['description']), 'classification': texts.get(difficulty).strip(), 'description': texts.get(dbr['itemText']), }) # Greater scroll of svefnthorn is incorrectly referenced as its Divine # variant. Manual fix required for now: if '02_svefnthorn.dbr' in str(dbr_file): result['tag'] = 'x2tagScrollName06' result['name'] = texts.get('x2tagScrollName06') # Set the bitmap if it exists if 'bitmap' in dbr: result['bitmap'] = dbr['bitmap'] # Grab the skill file: skill = {} try: skill = DBRParser.parse(dbr['skillName']) except InvalidItemError as e: logging.debug(f"Could not parse skill {dbr['skillName']} from " f"scroll {result['name']}. {e}") # Add the first tier of properties if there are any: if 'properties' in skill and skill['properties']: result['properties'] = skill['properties'][0] # Add any summon (just the first one) if 'summons' in skill: result['summons'] = skill['summons'][0]
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') difficulty = self.DIFFICULTIES_LIST[int(file_name[0][1:]) - 1] result.update({ # The act it starts dropping in is also listed in the file name 'act': file_name[1], # Bitmap has a different key name than items here. 'bitmap': dbr.get('relicBitmap', None), # Difficulty classification is based on the file name 'classification': texts.get(difficulty).strip(), # Ironically the itemText holds the actual description tag 'description': texts.get(dbr['itemText']), # For relics the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], }) # The possible completion bonuses are in bonusTableName: bonuses = {} try: bonuses = DBRParser.parse(dbr['bonusTableName']) except InvalidItemError as e: logging.debug("Could not parse relic completion bonus information " f"for {result['name']} in {dbr_file}. {e}") result['bonus'] = bonuses.get('table', []) # Find how many pieces this relic has max_pieces = TQDBParser.highest_tier( result['properties'], result['properties'].keys()) # Initialize a list of tiers properties = [{} for i in range(max_pieces)] # Setup properties as list to correspond to adding pieces of a relic: for key, values in result['properties'].items(): if not isinstance(values, list): # This property is just repeated for all tiers: for i in range(max_pieces): properties[i][key] = values continue for index, value in enumerate(values): properties[index][key] = value result['properties'] = properties
def parse(self, dbr, dbr_file, result): items = {} # Add up all the loot weights: summed = sum(v for k, v in dbr.items() if k.startswith('lootWeight')) # Run through all the loot entries and parse them: for i in range(1, 31): weight = dbr.get(f'lootWeight{i}', 0) # Skip items with no chance: if not weight: continue chance = float('{0:.5f}'.format(weight / summed)) try: # Try to parse the referenced loot file loot_file = dbr[f'lootName{i}'] except KeyError: logging.debug(f'No lootName{i} not found in {dbr_file}.') continue # Parse the loot file loot = DBRParser.parse( loot_file, # Always pass along any references that were set: result['references'], ) # e.g. xpack2\quests\rewards\loottables\generic_rareweapon_n.dbr # The entry lootName15 has two entries separated by ';' if 'loot_table' not in loot: logging.debug(f'Invalid lootName{i} in {dbr_file}.') continue # Loot entries will be in 'table', add those: for k, v in loot['loot_table'].items(): if k in items: items[k] += (v * chance) else: items[k] = (v * chance) # Add the parsed loot table result['loot_table'] = items
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') difficulty = self.DIFFICULTIES_LIST[int(file_name[0][1:]) - 1] result.update({ # The act it starts dropping in is also listed in the file name 'act': file_name[1], # Bitmap has a different key name than items here. 'bitmap': dbr.get('relicBitmap', None), # Difficulty classification is based on the file name 'classification': texts.get(difficulty).strip(), # Ironically the itemText holds the actual description tag 'description': texts.get(dbr['itemText']), # For relics the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], }) # The possible completion bonuses are in bonusTableName: bonuses = DBRParser.parse(dbr['bonusTableName']) result['bonus'] = bonuses.get('table', []) # Find how many pieces this relic has max_pieces = TQDBParser.highest_tier( result['properties'], result['properties'].keys()) # Initialize a list of tiers properties = [{} for i in range(max_pieces)] # Setup properties as list to correspond to adding pieces of a relic: for key, values in result['properties'].items(): if not isinstance(values, list): # This property is just repeated for all tiers: for i in range(max_pieces): properties[i][key] = values continue for index, value in enumerate(values): properties[index][key] = value result['properties'] = properties
def parse_loot(self, loot_key, spawn_number, dbr, result): chance = dbr.get(f'{loot_key}Chance', 0) # Skip slots that have 0 chance to drop if not chance: return # Add up all the loot weights: summed = sum(v for k, v in dbr.items() if k.startswith(f'{loot_key}Weight')) # Run through all the loot possibilities and parse them: for i in range(1, 7): weight = dbr.get(f'{loot_key}Weight{i}', 0) # Skip items with no chance: if not weight: continue try: loot = DBRParser.parse( dbr[f'{loot_key}Name{i}'][0], # Always pass along any references that were set: result['references'], ) # Parse the table and multiply the values by the chance: loot_chance = float('{0:.5f}'.format(weight / summed)) new_items = dict( (k, v * loot_chance * chance * spawn_number) for k, v in loot['loot_table'].items() ) except KeyError: # Skip files that weren't found/parsed (no loot_table) continue for k, v in new_items.items(): if k in self.items: self.items[k] += v else: self.items[k] = v
def _parse_skill_grant(self, dbr, result): """ Parse a granted skill. """ # If it doesn't have a granted skill name and level, it grants no skills: if self.SKILL_NAME not in dbr or self.SKILL_LEVEL not in dbr: return level = dbr[self.SKILL_LEVEL] try: skill = DBRParser.parse(dbr[self.SKILL_NAME]) except InvalidItemError as e: logging.debug( f"Skipping granted skill record in {dbr[self.SKILL_NAME]}. {e}" ) return if 'name' not in skill: return # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Now add the granted skill to the item: result['properties'][self.SKILL_NAME] = { 'tag': skill_tag, 'level': level, } if self.CONTROLLER not in dbr: return # Grab the auto controller to see if this skill is activated: controller = DBRParser.read(dbr[self.CONTROLLER]) # The property 'triggerType' can be converted to a useful text suffix # like 'Activated on attack': result['properties'][self.SKILL_NAME].update( {'trigger': texts.get(self.TRIGGERS[controller['triggerType']])})
def parse(self, dbr, dbr_file, result): """ Parse skill augments, mastery augments, and item granted skills. """ self._parse_skill_grant(dbr, result) # Parse skills that are augmented: for name, level in self.SKILL_AUGMENTS.items(): # Skip skills without both the name and level set: if name not in dbr or level not in dbr: continue if 'skilltree' in str(dbr[name]): # The atlantis expansion references skilltree instead of # skillmastery, so rebuild the path: mastery_file = str(dbr[name]).replace('skilltree', 'mastery') else: mastery_file = dbr[name] # Parse the skill: skill = DBRParser.parse(mastery_file) # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) level = dbr[level] # Skill format is either ItemSkillIncrement or ItemMasteryIncrement skill_format = (self.TXT_SKILL_INC if 'Mastery' not in skill['name'] else self.TXT_MASTERY_INC) result['properties'][name] = { 'tag': skill_tag, 'name': texts.get(skill_format).format(level, skill['name']), } # Parse augment to all skills: if self.AUGMENT_ALL in dbr: level = dbr[self.AUGMENT_ALL] result['properties'][self.AUGMENT_ALL] = (texts.get( self.TXT_ALL_INC).format(level))
def parse(self, dbr, dbr_file, result): """ Parse the scroll. """ # Use the file name to determine the difficulty: file_name = os.path.basename(dbr_file).split('_')[0][1:] # Strip all but digits from the string, then cast to int: difficulty = self.DIFFICULTIES_LIST[int( ''.join(filter(lambda x: x.isdigit(), file_name))) - 1] result.update({ 'tag': dbr['description'], 'name': texts.get(dbr['description']), 'classification': texts.get(difficulty).strip(), 'description': texts.get(dbr['itemText']), }) # Greater scroll of svefnthorn is incorrectly referenced as its Divine # variant. Manual fix required for now: if '02_svefnthorn.dbr' in str(dbr_file): result['tag'] = 'x2tagScrollName06' result['name'] = texts.get('x2tagScrollName06') # Set the bitmap if it exists if 'bitmap' in dbr: result['bitmap'] = dbr['bitmap'] # Grab the skill file: skill = DBRParser.parse(dbr['skillName']) # Add the first tier of properties if there are any: if 'properties' in skill and skill['properties']: result['properties'] = skill['properties'][0] # Add any summon (just the first one) if 'summons' in skill: result['summons'] = skill['summons'][0]
def parse_creatures(): """ Parse all creatures (bosses and heroes) in Titan Quest. Parsing the bosses and heroes is mostly about parsing their loot tables to create an index of what they can drop. This index will work two ways, the first being a complete list of items that the monster can drop and the reverse being added to each individual item's loot table so it can be sorted. """ timer = time.clock() files = [] for resource in resources.CREATURES: boss_files = resources.DB / resource files.extend(glob.glob(str(boss_files), recursive=True)) creatures = {} for dbr in files: parsed = parse(dbr) try: # Don't include common monsters # XXX - Should 'Champion' be added? if parsed['classification'] not in ['Quest', 'Hero', 'Boss']: continue # Store the monster by its tag: creatures[parsed['tag']] = parsed except KeyError: # Skip creatures without tags continue # Log the timer: logging.info(f'Parsed creatures in {time.clock() - timer} seconds.') return creatures
def parse(self, dbr, dbr_file, result): tables = {} weights = {} # Initialize the results table: result['table'] = [] # Parse all available entries for field, value in dbr.items(): if field.startswith('randomizerName'): # Grab the number suffix (1-70) number = re.search(r'\d+', field).group() # Store the DBR reference in the table tables[number] = value if field.startswith('randomizerWeight'): # Grab the number suffix (1-70) number = re.search(r'\d+', field).group() # Store the weight reference in the table weights[number] = value # Add all the weights together to determined % later total_weight = sum(weights.values()) for key, dbr_file in tables.items(): # Skip entries without chance or without a file if key not in weights or not os.path.exists(dbr_file): continue # Parse the table entry randomizer = DBRParser.parse(dbr_file) # Append the parsed bonus with its chance: result['table'].append({ 'chance': float('{0:.2f}'.format((weights[key] / total_weight) * 100)), 'option': randomizer['properties'] })
def parse(self, dbr, dbr_file, result): tables = {} weights = {} # Initialize the results table: result['table'] = [] # Parse all available entries for field, value in dbr.items(): if field.startswith('randomizerName'): # Grab the number suffix (1-70) number = re.search(r'\d+', field).group() # Store the DBR reference in the table tables[number] = value if field.startswith('randomizerWeight'): # Grab the number suffix (1-70) number = re.search(r'\d+', field).group() # Store the weight reference in the table weights[number] = value # Add all the weights together to determined % later total_weight = sum(weights.values()) for key, dbr_file in tables.items(): # Skip entries without chance or without a file if key not in weights or not os.path.exists(dbr_file): continue # Parse the table entry randomizer = DBRParser.parse(dbr_file) # Append the parsed bonus with its chance: result['table'].append({ 'chance': float( '{0:.2f}'.format((weights[key] / total_weight) * 100)), 'option': randomizer['properties'] })
def parse(self, dbr, dbr_file, result): """ Parse the monster. """ self.parse_creature(dbr, dbr_file, result) # Don't parse any further for tagless or level-less creatures: if 'tag' not in result or not result['level']: return # Iterate over normal, epic & legendary version of the boss: loot = [] for index in range(3): # Initialize an empty result: loot.append({}) # Create a DBR that only has the equipment for this difficulty: difficulty_dbr = TQDBParser.extract_values(dbr, '', index) # Parse all the equipment in this difficulty difficulty_dbr = self.parse_difficulty(difficulty_dbr, dbr_file) # Only store the equipment if there was any: if difficulty_dbr: loot[index] = difficulty_dbr # If there is any tiered data to store, store it: if any(tier for tier in loot if tier): result['loot'] = loot chests = [] tag = result['tag'] # Find the chest for each difficulty: for index in range(3): # Initialize an empty result: chests.append({}) # Grab the chest to parse: if tag in CHESTS and CHESTS[tag][index]: # Grab the level for this index, or the last one: level = ( result['level'][index] if len(result['level']) > index else result['level'][-1]) # Parse the chest and pass the monsters level as a reference: loot = DBRParser.parse( DB / CHESTS[tag][index], {'level': level}, ) # Convert all item chances to 4 point precision: chests[index] = dict( (k, float('{0:.4f}'.format(v))) for k, v in loot['loot_table'].items()) # If there is any tiered data to store, store it: if any(tier for tier in chests if tier): result['chest'] = chests # Check if this monster is limited to a difficulty: if len(result['level']) != len(set(result['level'])): # If a level is repeated, it means a creature doesn't spawn in # some difficulties. The 'normal' difficulty level is either # repeated in Epic and Legendary, so find the index and subtract # 1 from that to get all difficulties that should be removed: for i in range(result['level'].count(result['level'][0]) - 1): result['properties'][i] = {} result['abilities'][i] = {} result['level'][i] = None
def parse(self, dbr, dbr_file, result): """ Parse the skill that spawns a pet. """ # Only set time to live it's set (otherwise it's infinite) ttl_list = dbr.get(self.TTL, None) # Parse all the summons and set them as a list: result['summons'] = [] for index, spawn_file in enumerate(dbr['spawnObjects']): spawn = DBRParser.parse(spawn_file) # Keep track of the original properties this summon had: original_properties = {} if 'properties' in spawn: if isinstance(spawn['properties'], list): original_properties = spawn['properties'][0].copy() else: original_properties = spawn['properties'].copy() # We need the raw values from the spawn DBR for hp/mp spawn['properties'] = {} spawn_dbr = DBRParser.read(spawn_file) if 'characterLife' in spawn_dbr: hp_list = spawn_dbr['characterLife'] hp = ( hp_list[index] if index < len(hp_list) else hp_list[len(hp_list) - 1]) TQDBParser.insert_value( 'characterLife', texts.get('LifeText').format(hp), spawn) if 'characterMana' in spawn_dbr: mp_list = spawn_dbr['characterMana'] mp = ( mp_list[index] if index < len(mp_list) else mp_list[len(mp_list) - 1]) TQDBParser.insert_value( 'characterMana', texts.get('ManaText').format(mp), spawn) if ttl_list: ttl = ( ttl_list[index] if index < len(ttl_list) else ttl_list[len(ttl_list) - 1]) TQDBParser.insert_value( self.TTL, texts.get(self.TTL).format(ttl), spawn) # Iterate over the original properties and add some whitelisted # properties to the final result: for key, value in original_properties.items(): if key.startswith('character'): continue spawn['properties'][key] = value result['summons'].append(spawn)
def parse_affixes(): """ Parse all the Titan Quest affixes. Affixes are the pre- and suffixes that are applied to weapons. These affixes add properties to the equipment, these properties, the affix names and the equipment they can be applied to is indexed and parsed in this function. """ timer = time.clock() files = [] for resource in resources.AFFIX_TABLES: table_files = resources.DB / resource files.extend(glob.glob(str(table_files), recursive=True)) # The affix tables will determine what gear an affix can be applied to. affix_tables = {} for dbr in files: table = read(dbr) # Use the filename to determine what equipment this table is for: file_name = os.path.basename(dbr).split('_') table_type = get_affix_table_type(file_name[0]) # For each affix in this table, create an entry: for field, affix_dbr in table.items(): if not field.startswith('randomizerName') or not table_type: continue affix_dbr = str(affix_dbr) if affix_dbr not in affix_tables: affix_tables[affix_dbr] = [table_type] elif table_type not in affix_tables[affix_dbr]: affix_tables[affix_dbr].append(table_type) files = [] for resource in resources.AFFIXES: affix_files = resources.DB / resource files.extend(glob.glob(str(affix_files), recursive=True)) affixes = {'prefixes': {}, 'suffixes': {}} for dbr in files: affix = parse(dbr) # Skip affixes without properties (first one will be empty): if not affix['properties']: continue # Skip the incorrect 'of the Mammoth' prefix entry: if 'prefix' in dbr and affix['tag'] == 'tagPrefix145': continue # Assign the table types to this affix: if dbr not in affix_tables: # Affix can occur on all equipment: affix['equipment'] = 'none' else: affix['equipment'] = ','.join(affix_tables[dbr]) # Add affixes to their respective pre- or suffix list. if 'Prefix' in affix['tag'] and 'suffix' not in dbr: affixType = 'prefixes' else: affixType = 'suffixes' affixTag = affix.pop('tag') # Either add the affix or add its properties as an alternative if affixTag in affixes[affixType]: # Skip duplicate affix properties: if is_duplicate_affix(affixes[affixType][affixTag], affix): continue affixes[affixType][affixTag]['properties'].append( affix['properties']) else: # Place the affix properties into a list that can be extended by # alternatives during this parsing. affix['properties'] = [affix['properties']] affixes[affixType][affixTag] = affix # Log and reset the timer: logging.info(f'Parsed affixes in {time.clock() - timer} seconds.') return affixes
def parse_quests(): """ Parse the Titan Quest quest rewards. The quest rewards are indexed by creating a text readable version of the QST files located in the Resources/Quests.arc file. The rewards are extracted by only retrieving rewards prefixed with item[] tags. """ timer = time.clock() # Regex to find item rewards REWARD = re.compile( r'item\[(?P<index>[0-9])\](.{0,1})' r'(?P<file>' 'records' r'[\\||\/]' r'(xpack[2]?[\\||\/])?' 'quests' r'[\\||\/]' 'rewards' r'[\\||\/]' r'([^.]+)\.dbr' r')' ) # Regex to find the title tag TITLE = re.compile(r'titletag(?P<tag>[^\s]*)') files = glob.glob(resources.QUESTS) quests = {} for qst in files: with open(qst, 'rb') as quest: # Read the content as printable characters only: content = ''.join( c for c in # Lower case and convert to utf-8 quest.read().decode('utf-8', errors='ignore').lower() if c in string.printable ) # Find the title and skip this file if none is found: title_tag = TITLE.search(content) if not title_tag or not title_tag.group('tag'): continue # Grab the quest title tag tag = title_tag.group('tag') if tag not in quests: # Initialize three difficulties: quests[tag] = { 'name': texts.get(tag), 'rewards': [{}, {}, {}], } # Parsed reward files (so we don't duplicate): parsed = [] # Add all the rewards to the quest: for match in REWARD.finditer(content): # The index in the item[index] tag determines the difficulty: difficulty = int(match.group('index')) reward_file = match.group('file') # Store the file or move on if we've already parsed it if reward_file not in parsed: parsed.append(reward_file) else: continue # Prepend the path with the database path: rewards = parse(resources.DB / reward_file) # Skip quests where the rewards aren't items: if 'loot_table' not in rewards: continue # Either set the chance or add it to a previous chance: for item, chance in rewards['loot_table'].items(): if item in quests[tag]['rewards'][difficulty]: quests[tag]['rewards'][difficulty][item] += chance else: quests[tag]['rewards'][difficulty][item] = chance # Don't save quests without item rewards: if not any(reward for reward in quests[tag]['rewards']): quests.pop(tag) # Turn all chances into percentages: for tag, quest in quests.items(): for index, difficulty in enumerate(quest['rewards']): for item, chance in difficulty.items(): # Format into 4 point precision percentages: quests[tag]['rewards'][index][item] = ( float('{0:.4f}'.format(chance * 100))) # Log the timer: logging.info(f'Parsed quest rewards in {time.clock() - timer} seconds.') return quests
def parse_equipment(): """ Parse all wearable Titan Quest equipment. The wearable equipment is indexed and sorted by equipment type. These categories are defined by the Class property of each piece of equipment which is mapped to the 'category' key in the parsed result. :return: dictionary keyed by equipment category string, value is a list of dicts, one for each item in that category. Common items are omitted. """ start_time = time.time() files = [] for resource in resources.EQUIPMENT: equipment_files_globpath = resources.DB / resource for equipment_filename in glob.glob(str(equipment_files_globpath), recursive=True): equipment_path = Path(equipment_filename) posix_path = equipment_path.as_posix() if not ( # Exclude all files in 'old' and 'default' 'old' in equipment_path.parts or 'default' in equipment_path.parts or # Rhodian and Electrum sling don't drop: posix_path.endswith('/1hranged/u_e_02.dbr') or posix_path.endswith('/1hranged/u_n_05.dbr')): files.append(equipment_filename) logging.info(f"Found {len(files)} equipment files to process.") # TODO: add multithreading! items = defaultdict(list) for dbr in files: try: parsed = parse(dbr) except InvalidItemError as e: exception_messages = exception_messages_with_causes(e) logging.debug(f"Ignoring item in {dbr}. {exception_messages}") continue try: # Skip items without a category if 'category' not in parsed: continue # Organize the equipment based on its category category = parsed.pop('category') # Skip items without rarities if 'classification' not in parsed: continue # Save the bitmap and remove the bitmap key images.save_bitmap(parsed, category, 'output/graphics/') except KeyError as e: # Skip equipment that couldn't be parsed: logging.warning( f"DBR {dbr} parse result unacceptable. Parse result: {parsed}. Error: {e}" ) # raise e continue # Pop off the properties key off any item without properties: if 'properties' in parsed and not parsed['properties']: parsed.pop('properties') # Now save the parsed item in the category: if category: items[category].append(parsed) # Log the timer: logging.info( f'Parsed equipment in {time.time() - start_time:.2f} seconds.') return items
def parse_quests(): """ Parse the Titan Quest quest rewards. The quest rewards are indexed by creating a text readable version of the QST files located in the Resources/Quests.arc file. The rewards are extracted by only retrieving rewards prefixed with item[] tags. """ timer = time.clock() # Regex to find item rewards REWARD = re.compile(r'item\[(?P<index>[0-9])\](.{0,1})' r'(?P<file>' 'records' r'[\\||\/]' r'(xpack[2]?[\\||\/])?' 'quests' r'[\\||\/]' 'rewards' r'[\\||\/]' r'([^.]+)\.dbr' r')') # Regex to find the title tag TITLE = re.compile(r'titletag(?P<tag>[^\s]*)') files = glob.glob(resources.QUESTS) quests = {} for qst in files: with open(qst, 'rb') as quest: # Read the content as printable characters only: content = ''.join( c for c in # Lower case and convert to utf-8 quest.read().decode('utf-8', errors='ignore').lower() if c in string.printable) # Find the title and skip this file if none is found: title_tag = TITLE.search(content) if not title_tag or not title_tag.group('tag'): continue # Grab the quest title tag tag = title_tag.group('tag') if tag not in quests: # Initialize three difficulties: quests[tag] = { 'name': texts.get(tag), 'rewards': [{}, {}, {}], } # Parsed reward files (so we don't duplicate): parsed = [] # Add all the rewards to the quest: for match in REWARD.finditer(content): # The index in the item[index] tag determines the difficulty: difficulty = int(match.group('index')) reward_file = match.group('file') # Store the file or move on if we've already parsed it if reward_file not in parsed: parsed.append(reward_file) else: continue # Prepend the path with the database path: rewards = parse(resources.DB / reward_file) # Skip quests where the rewards aren't items: if 'loot_table' not in rewards: continue # Either set the chance or add it to a previous chance: for item, chance in rewards['loot_table'].items(): if item in quests[tag]['rewards'][difficulty]: quests[tag]['rewards'][difficulty][item] += chance else: quests[tag]['rewards'][difficulty][item] = chance # Don't save quests without item rewards: if not any(reward for reward in quests[tag]['rewards']): quests.pop(tag) # Turn all chances into percentages: for tag, quest in quests.items(): for index, difficulty in enumerate(quest['rewards']): for item, chance in difficulty.items(): # Format into 4 point precision percentages: quests[tag]['rewards'][index][item] = (float('{0:.4f}'.format( chance * 100))) # Log the timer: logging.info(f'Parsed quest rewards in {time.clock() - timer} seconds.') return quests