def update_all(): "Updates all supported entity types using merged chunk data from ingame binaries." from mhdata.binary import metadata from mhdata.binary import ItemCollection, ArmorCollection, MonsterCollection from mhdata.load import load_data from .armor import update_armor, update_charms from .weapons import update_weapons, update_weapon_songs, update_kinsects from .monsters import update_monsters from .quests import update_quests from .items import update_items, update_decorations, register_combinations, ItemUpdater from . import simple_translate mhdata = load_data() print("Existing Data loaded. Using it as a base to merge new data") area_map = metadata.load_area_map() print("Area Map Loaded") # validate area map error = False for name in area_map.values(): if name not in mhdata.location_map.names('en'): print(f"Error: Area map has invalid location name {name}.") error = True if error: return print("Area Map validated") item_data = ItemCollection() armor_data = ArmorCollection() monster_data = MonsterCollection() item_updater = ItemUpdater(item_data) print() # newline simple_translate.translate_skills(mhdata) update_monsters(mhdata, item_data, monster_data) update_armor(mhdata, item_updater, armor_data) update_charms(mhdata, item_updater, armor_data) update_weapons(mhdata, item_updater) update_decorations(mhdata, item_data) #update_weapon_songs(mhdata) #update_kinsects(mhdata, item_updater) #update_quests(mhdata, item_updater, monster_meta, area_map) # Now finalize the item updates from parsing the rest of the data register_combinations(mhdata, item_updater) update_items(item_updater)
def read_status(monsters: MonsterCollection): "Reads status data for all monsters in the form of a nested didctionary, indexed by the binary monster id" root = Path(get_chunk_root()) results = {} for filename in root.joinpath('em/').rglob('*.dtt_eda'): eda_binary = load_eda(filename) json_data = struct_to_json(eda_binary) try: name = monsters.by_id(eda_binary.monster_id).name['en'] results[eda_binary.monster_id] = { 'name': name, 'filename': str(filename.relative_to(root)), **json_data } except KeyError: pass # warn? return results
def get_quest_data(quest, item_updater: ItemUpdater, monster_data: MonsterCollection, area_map): "Returns a dictionary of resolved quest data, marking items used in the item updater" binary = quest.binary if binary.header.mapId == 0: print(quest.name) if binary.header.mapId not in area_map: raise Exception( f'Failed to retrieve map {binary.header.mapId} for quest {quest.name["en"]}' ) result = { 'id': quest.id, 'name': quest.name, 'objective': quest.objective, 'description': quest.description, 'rank': quest.rank, 'stars': quest.stars, 'location_en': area_map[binary.header.mapId], 'quest_type': None, 'zenny': binary.header.zennyReward, 'monsters': [], 'rewards': [] } monster_entries = {} def get_monster_name(monster_id): try: return monster_data.by_id(monster_id).name['en'] except KeyError: raise Exception( f'Failed to retrieve monster {monster_id} for quest {quest.name["en"]}' ) def has_monster(monster_id): return monster_entries.get(get_monster_name(monster_id)) def add_monster(monster_id, quantity, objective=False): monster_name = get_monster_name(monster_id) existing_entry = monster_entries.get(monster_name) if existing_entry: existing_entry['quantity'] += quantity else: entry = { 'monster_en': monster_name, 'quantity': quantity, 'is_objective': objective } result['monsters'].append(entry) monster_entries[monster_name] = entry # Note about quest type: # The values are powers of 2, so it could be a bitwise flag system, # however only one bit is ever set at a time. Check again after Iceborne # 1 - Hunt # 2 - small monsters # 4 - capture # 8 - delivery (no monsters are objectives) # 16 - hunt N monsters # 32 - probably special story # Note about objective types: # 0 - seems to be when there is no data # 1 (with event 4) - Hunt N monsters # 2 - item turn in # 17 - capture # 33 - small monster # 49 - large monster # Using objective type the logic becomes simpler (flag as we go) however we can fall back to quest type if we must quest_type = binary.objective.quest_type objectives = binary.objective.objectives # if subobjectives have a value, use the sub objective instead of the main objectives # This only happens in Kestodon Kerfluffle. The logic may have to change for Iceborne. if binary.objective.sub_objectives[0].objective_id != 0: objectives = binary.objective.sub_objectives # Go through objectives, setting the quest type and adding monsters for obj in objectives: if obj.objective_type == 0: continue # Set the quest type. # Because of the existance of type 32, we use the objective type to figure out the quest type if result['quest_type'] is None: if (obj.objective_type == 1 and obj.event == 4) or obj.objective_type in [33, 49]: result['quest_type'] = 'hunt' elif obj.objective_type == 17: result['quest_type'] = 'capture' elif obj.objective_type == 2: result['quest_type'] = 'deliver' else: raise Exception( f"Unknown objective type {obj.objective_type} in quest {quest.name['en']}" ) if obj.objective_type in [17, 33, 49]: add_monster(obj.objective_id, obj.objective_amount, True) elif obj.objective_type == 1 and obj.event == 4: for i in range(obj.objective_amount): add_monster(binary.monsters[i].monster_id, 1, True) elif obj.objective_type == 2: pass # item turn in, not handled yet # Now add the remaining monsters for monster in binary.monsters: monster_id = monster.monster_id if monster_id == -1 or has_monster(monster_id): continue # Kulve Taroth is a special exception monster_name = monster_data.by_id(monster_id).name['en'] if monster_name in ['Kulve Taroth', 'Zorah Magdaros']: result['quest_type'] = 'assignment' add_monster(monster_id, 1, True) else: add_monster(monster_id, 1) # quest rewards try: rewards = [] for idx, rem in enumerate(quest.reward_data_list): group = ascii_uppercase[idx] group_items = [] first = True for (item_id, qty, chance) in rem.iter_items(): item_name, _ = item_updater.name_and_description_for(item_id) if first and not rem.drop_mechanic: rewards.append({ 'group': group, 'item_en': item_name['en'], 'stack': qty, 'percentage': 100 }) first = False group_items.append({ 'item_en': item_name['en'], 'stack': qty, 'percentage': chance }) def rank_drop(drop): percentage = drop['percentage'] if percentage == 100: return 0 return 100 - percentage group_items.sort(key=rank_drop) rewards.extend({'group': group, **i} for i in group_items) result['rewards'] = rewards except DummyItemError: print( f"ERROR: Quest {quest.id}:{quest.name['en']} has invalid items, skipping rewards" ) # more special exceptions if quest.name['en'] in ['The Legendary Beast']: result['quest_type'] = 'hunt' return result
def write_quest_raw_data(quests, item_data: ItemCollection, monster_data: MonsterCollection): "Writes the artifact file for the quest" quest_artifact_entries = [] quest_monsters_artifact_entries = [] quest_reward_artifact_entries = [] # Internal helper to add a prefix to "unk" fields def prefix_unk_fields(basename, d): result = {} for key, value in d.items(): if key.startswith('unk'): key = basename + '_' + key result[key] = value return result def prefix_fields(prefix, d): return {f'{prefix}{k}': v for k, v in d.items()} for quest in quests: binary = quest.binary quest_artifact_entries.append({ 'id': quest.id, 'name_en': quest.name['en'], **flatten_dict(struct_to_json(binary.header), 'header'), **flatten_dict(struct_to_json(binary.objective), 'obj') }) # handle monsters for monster_mib in quest.binary.monsters: if monster_mib.monster_id == -1: continue monster_name = None try: monster_name = monster_data.by_id( monster_mib.monster_id).name['en'] except KeyError: pass quest_monsters_artifact_entries.append({ 'id': quest.id, 'name_en': quest.name['en'], 'monster_name': monster_name, **monster_mib.as_dict(), }) # handle rewards for idx, rem in enumerate(quest.reward_data_list): first = True for (item_id, qty, chance) in rem.iter_items(): item_name = item_data.by_id(item_id).name if first and not rem.drop_mechanic: quest_reward_artifact_entries.append({ 'id': quest.id, 'name_en': quest.name['en'], 'reward_idx': idx, 'signature?': rem.signature, 'signatureExt?': rem.signatureExt, 'drop_mechanic': rem.drop_mechanic, 'item_name': item_name['en'], 'qty': qty, 'chance': 100 }) quest_reward_artifact_entries.append({ 'id': quest.id, 'name_en': quest.name['en'], 'reward_idx': idx, 'signature?': rem.signature, 'signatureExt?': rem.signatureExt, 'drop_mechanic': rem.drop_mechanic, 'item_name': item_name['en'], 'qty': qty, 'chance': chance }) first = False write_dicts_artifact('quest_raw_data.csv', quest_artifact_entries) write_dicts_artifact('quest_raw_monsters.csv', quest_monsters_artifact_entries) write_dicts_artifact('quest_raw_rewards.csv', quest_reward_artifact_entries)
def update_monsters(mhdata, item_data: ItemCollection, monster_data: MonsterCollection): root = Path(get_chunk_root()) # Load hitzone entries. EPG files contain hitzones, parts, and base hp print('Loading monster hitzone data') if monster_data.load_epg_eda(): print('Loaded Monster epg data (hitzones and breaks)') # Load status entries monster_statuses = read_status(monster_data) print('Loaded Monster status data') # Write hitzone data to artifacts hitzone_raw_data = [{ 'name': m.name['en'], **struct_to_json(m.epg) } for m in monster_data.monsters if m.epg is not None] artifacts.write_json_artifact("monster_hitzones_and_breaks.json", hitzone_raw_data) print( "Monster hitzones+breaks raw data artifact written (Automerging not supported)" ) write_hitzone_artifacts(monster_data) print("Monster hitzones artifact written (Automerging not supported)") artifacts.write_json_artifact("monster_status.json", list(monster_statuses.values())) print("Monster status artifact written (Automerging not supported)") monster_drops = read_drops(monster_data, item_data) print('Loaded Monster drop rates') for monster_entry in mhdata.monster_map.values(): name_en = monster_entry.name('en') try: monster = monster_data.by_name(name_en) except KeyError: print(f'Warning: Monster {name_en} not in metadata, skipping') continue monster_entry['name'] = monster.name if monster.description: monster_entry['description'] = monster.description # Compare drops (use the hunting notes key name if available) drop_tables = monster_drops.get(monster.id, None) if drop_tables: # Write drops to artifact files joined_drops = [] for idx, drop_table in enumerate(drop_tables): joined_drops.extend({ 'group': f'Group {idx+1}', **e } for e in drop_table) artifacts.write_dicts_artifact( f'monster_drops/{name_en} drops.csv', joined_drops) # Check if any drop table in our current database is invalid if 'rewards' in monster_entry: rewards_sorted = sorted(monster_entry['rewards'], key=itemgetter('condition_en')) rewards = itertools.groupby(rewards_sorted, key=lambda r: (r['condition_en'], r['rank'])) for (condition, rank), existing_table in rewards: if condition in itlot_conditions: existing_table = list(existing_table) if not any( compare_drop_tables(existing_table, table) for table in drop_tables): print( f"Validation Error: Monster {name_en} has invalid drop table {condition} in {rank}" ) else: print(f'Warning: no drops file found for monster {name_en}') # Compare hitzones hitzone_data = monster.hitzones if hitzone_data and 'hitzones' in monster_entry: # Create tuples of the values of the hitzone, to use as a comparator hitzone_key = lambda h: tuple(h[v] for v in hitzone_fields) stored_hitzones = [hitzone_key(h) for h in hitzone_data] stored_hitzones_set = set(stored_hitzones) # Check if any hitzone we have doesn't actually exist for hitzone in monster_entry['hitzones']: if hitzone_key(hitzone) not in stored_hitzones_set: print( f"Validation Error: Monster {name_en} has invalid hitzone {hitzone['hitzone']['en']}" ) elif 'hitzones' not in monster_entry and hitzone_data: print( f'Warning: no hitzones in monster entry {name_en}, but binary data exists' ) elif 'hitzones' in monster_entry: print( f'Warning: hitzones exist in monster {name_en}, but no binary data exists to compare' ) else: print(f"Warning: No hitzone data for monster {name_en}") # Status info status = monster_statuses.get(monster.id, None) if status: test = lambda v: v['base'] > 0 and v['decrease'] > 0 monster_entry['pitfall_trap'] = True if test( status['pitfall_trap_buildup']) else False monster_entry['shock_trap'] = True if test( status['shock_trap_buildup']) else False monster_entry['vine_trap'] = True if test( status['vine_trap_buildup']) else False # Write new data writer = create_writer() writer.save_base_map_csv( "monsters/monster_base.csv", mhdata.monster_map, schema=schema.MonsterBaseSchema(), translation_filename="monsters/monster_base_translations.csv", translation_extra=['description']) print("Monsters updated\n")