Esempio n. 1
0
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)
Esempio n. 2
0
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
Esempio n. 3
0
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
Esempio n. 4
0
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)
Esempio n. 5
0
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")