import os
import json
import random
import logging
import shutil
from collections import defaultdict
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from palworld_save_tools.archive import UUID
from PySide6.QtWidgets import QMessageBox, QInputDialog
from i18n import t
try:
    from palworld_aio import constants
    from palworld_aio.utils import sav_to_json, json_to_sav, sav_to_gvasfile, gvasfile_to_sav, are_equal_uuids, as_uuid, is_valid_level, extract_value, format_duration, sanitize_filename
    from palworld_aio.data_manager import delete_base_camp
except ImportError:
    from . import constants
    from .utils import sav_to_json, json_to_sav, sav_to_gvasfile, gvasfile_to_sav, are_equal_uuids, as_uuid, is_valid_level, extract_value, format_duration, sanitize_filename
    from .data_manager import delete_base_camp
def build_player_levels():
    if not constants.loaded_level_json:
        return
    char_map = constants.loaded_level_json['properties']['worldSaveData']['value'].get('CharacterSaveParameterMap', {}).get('value', [])
    uid_level_map = defaultdict(lambda: '?')
    for entry in char_map:
        try:
            sp = entry['value']['RawData']['value']['object']['SaveParameter']
            if sp['struct_type'] != 'PalIndividualCharacterSaveParameter':
                continue
            sp_val = sp['value']
            if not sp_val.get('IsPlayer', {}).get('value', False):
                continue
            key = entry.get('key', {})
            uid_obj = key.get('PlayerUId', {})
            uid = str(uid_obj.get('value', '') if isinstance(uid_obj, dict) else uid_obj)
            level = extract_value(sp_val, 'Level', '?')
            if uid:
                uid_level_map[uid.replace('-', '')] = level
        except:
            continue
    constants.player_levels = dict(uid_level_map)
def delete_player_pals(wsd, to_delete_uids):
    char_save_map = wsd.get('CharacterSaveParameterMap', {}).get('value', [])
    removed_pals = 0
    uids_set = {uid.replace('-', '') for uid in to_delete_uids if uid}
    new_map = []
    for entry in char_save_map:
        try:
            val = entry['value']['RawData']['value']['object']['SaveParameter']['value']
            struct_type = entry['value']['RawData']['value']['object']['SaveParameter']['struct_type']
            owner_uid = val.get('OwnerPlayerUId', {}).get('value')
            if owner_uid:
                owner_uid = str(owner_uid).replace('-', '')
            if struct_type in ('PalIndividualCharacterSaveParameter', 'PlayerCharacterSaveParameter') and owner_uid in uids_set:
                removed_pals += 1
                continue
        except:
            pass
        new_map.append(entry)
    wsd['CharacterSaveParameterMap']['value'] = new_map
    return removed_pals
def clean_character_save_parameter_map(data_source, valid_uids):
    if 'CharacterSaveParameterMap' not in data_source:
        return
    entries = data_source['CharacterSaveParameterMap'].get('value', [])
    keep = []
    for entry in entries:
        key = entry.get('key', {})
        value = entry.get('value', {}).get('RawData', {}).get('value', {})
        saveparam = value.get('object', {}).get('SaveParameter', {}).get('value', {})
        owner_uid_obj = saveparam.get('OwnerPlayerUId')
        if owner_uid_obj is None:
            keep.append(entry)
            continue
        owner_uid = owner_uid_obj.get('value', '')
        no_owner = owner_uid in ('', '00000000-0000-0000-0000-000000000000')
        player_uid = key.get('PlayerUId', {}).get('value', '')
        if player_uid and str(player_uid).replace('-', '') in valid_uids or str(owner_uid).replace('-', '') in valid_uids or no_owner:
            keep.append(entry)
    entries[:] = keep
def delete_empty_guilds(parent=None):
    if not constants.loaded_level_json:
        return 0
    build_player_levels()
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    group_data = wsd['GroupSaveDataMap']['value']
    to_delete = []
    for g in group_data:
        if g['value']['GroupType']['value']['value'] != 'EPalGroupType::Guild':
            continue
        gid_str = str(g['key'])
        gid_clean = gid_str.replace('-', '').lower()
        if any((gid_clean == ex.replace('-', '').lower() for ex in constants.exclusions.get('guilds', []))):
            continue
        players = g['value']['RawData']['value'].get('players', [])
        if not players:
            to_delete.append(g)
            continue
        all_invalid = True
        for p in players:
            if isinstance(p, dict) and 'player_uid' in p:
                uid_obj = p['player_uid']
                if hasattr(uid_obj, 'hex'):
                    uid = uid_obj.hex
                else:
                    uid = str(uid_obj)
            else:
                uid = str(p)
            uid = uid.replace('-', '')
            level = constants.player_levels.get(uid, None)
            if is_valid_level(level):
                all_invalid = False
                break
        if all_invalid:
            to_delete.append(g)
    for g in to_delete:
        gid = as_uuid(g['key'])
        bases = wsd.get('BaseCampSaveData', {}).get('value', [])[:]
        for b in bases:
            if are_equal_uuids(b['value']['RawData']['value'].get('group_id_belong_to'), gid):
                delete_base_camp(b, gid)
        group_data.remove(g)
    return len(to_delete)
def delete_inactive_players(days_threshold, parent=None):
    if not constants.loaded_level_json:
        return 0
    build_player_levels()
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    tick_now = wsd['GameTimeSaveData']['value']['RealDateTimeTicks']['value']
    group_data_list = wsd['GroupSaveDataMap']['value']
    deleted_info = []
    to_delete_uids = set()
    total_players_before = sum((len(g['value']['RawData']['value'].get('players', [])) for g in group_data_list if g['value']['GroupType']['value']['value'] == 'EPalGroupType::Guild'))
    excluded_players = {ex.replace('-', '') for ex in constants.exclusions.get('players', [])}
    for group in group_data_list[:]:
        if group['value']['GroupType']['value']['value'] != 'EPalGroupType::Guild':
            continue
        raw = group['value']['RawData']['value']
        original_players = raw.get('players', [])
        keep_players = []
        admin_uid = str(raw.get('admin_player_uid', '')).replace('-', '')
        for player in original_players:
            uid_obj = player.get('player_uid', '')
            uid = str(uid_obj.get('value', '') if isinstance(uid_obj, dict) else uid_obj).replace('-', '')
            if uid in excluded_players:
                keep_players.append(player)
                continue
            player_name = player.get('player_info', {}).get('player_name', 'Unknown')
            last_online = player.get('player_info', {}).get('last_online_real_time')
            level = constants.player_levels.get(uid)
            inactive = last_online is not None and (tick_now - last_online) / 864000000000 >= days_threshold
            if inactive or not is_valid_level(level):
                reason = 'Inactive' if inactive else 'Invalid level'
                extra = f' - Inactive for {format_duration((tick_now - last_online) / 10000000.0)}' if inactive and last_online else ''
                deleted_info.append(f'{player_name}({uid})- {reason}{extra}')
                to_delete_uids.add(uid)
            else:
                keep_players.append(player)
        if len(keep_players) != len(original_players):
            raw['players'] = keep_players
            keep_uids = {str(p.get('player_uid', '')).replace('-', '') for p in keep_players}
            if not keep_players:
                gid = group['key']
                base_camps = wsd.get('BaseCampSaveData', {}).get('value', [])
                for b in base_camps[:]:
                    if are_equal_uuids(b['value']['RawData']['value'].get('group_id_belong_to'), gid):
                        delete_base_camp(b, gid)
                group_data_list.remove(group)
            elif admin_uid not in keep_uids:
                raw['admin_player_uid'] = keep_players[0]['player_uid']
    if to_delete_uids:
        constants.files_to_delete.update(to_delete_uids)
        removed_pals = delete_player_pals(wsd, to_delete_uids)
        char_map = wsd.get('CharacterSaveParameterMap', {}).get('value', [])
        char_map[:] = [entry for entry in char_map if str(entry.get('key', {}).get('PlayerUId', {}).get('value', '')).replace('-', '') not in to_delete_uids and str(entry.get('value', {}).get('RawData', {}).get('value', {}).get('object', {}).get('SaveParameter', {}).get('value', {}).get('OwnerPlayerUId', {}).get('value', '')).replace('-', '') not in to_delete_uids]
        total_players_after = sum((len(g['value']['RawData']['value'].get('players', [])) for g in group_data_list if g['value']['GroupType']['value']['value'] == 'EPalGroupType::Guild'))
    return len(to_delete_uids)
def delete_inactive_bases(days_threshold, parent=None):
    if not constants.loaded_level_json:
        return 0
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    tick = wsd['GameTimeSaveData']['value']['RealDateTimeTicks']['value']
    inactive_guild_ids = []
    for g in wsd['GroupSaveDataMap']['value']:
        if g['value']['GroupType']['value']['value'] != 'EPalGroupType::Guild':
            continue
        gid = as_uuid(g['key'])
        players = g['value']['RawData']['value'].get('players', [])
        if not players:
            inactive_guild_ids.append(gid)
            continue
        all_inactive = True
        for p in players:
            last_online = p.get('player_info', {}).get('last_online_real_time')
            if last_online is None or (tick - last_online) / 10000000.0 / 86400 < days_threshold:
                all_inactive = False
                break
        if all_inactive:
            inactive_guild_ids.append(gid)
    base_list = wsd.get('BaseCampSaveData', {}).get('value', [])
    removed = 0
    excluded_bases = {ex.replace('-', '').lower() for ex in constants.exclusions.get('bases', [])}
    for b in base_list[:]:
        gid = as_uuid(b['value']['RawData']['value'].get('group_id_belong_to'))
        base_id = as_uuid(b['key'])
        if base_id.replace('-', '').lower() in excluded_bases:
            continue
        if gid in inactive_guild_ids:
            if delete_base_camp(b, gid):
                removed += 1
    return removed
def delete_duplicated_players(parent=None):
    if not constants.current_save_path or not constants.loaded_level_json:
        return 0
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    tick_now = wsd['GameTimeSaveData']['value']['RealDateTimeTicks']['value']
    group_data_list = wsd['GroupSaveDataMap']['value']
    uid_to_player = {}
    uid_to_group = {}
    deleted_players = []
    format_duration_lambda = lambda ticks: f'{int(ticks / 864000000000)}d:{int(ticks % 864000000000 / 36000000000)}h:{int(ticks % 36000000000 / 600000000)}m ago'
    for group in group_data_list:
        if group['value']['GroupType']['value']['value'] != 'EPalGroupType::Guild':
            continue
        raw = group['value']['RawData']['value']
        players = raw.get('players', [])
        filtered_players = []
        for player in players:
            uid_raw = player.get('player_uid', '')
            uid = str(uid_raw.get('value', '') if isinstance(uid_raw, dict) else uid_raw).replace('-', '')
            last_online = player.get('player_info', {}).get('last_online_real_time') or 0
            player_name = player.get('player_info', {}).get('player_name', 'Unknown')
            days_inactive = (tick_now - last_online) / 864000000000 if last_online else float('inf')
            if uid in uid_to_player:
                prev = uid_to_player[uid]
                prev_group = uid_to_group[uid]
                prev_lo = prev.get('player_info', {}).get('last_online_real_time') or 0
                prev_days_inactive = (tick_now - prev_lo) / 864000000000 if prev_lo else float('inf')
                prev_name = prev.get('player_info', {}).get('player_name', 'Unknown')
                if days_inactive > prev_days_inactive:
                    deleted_players.append({'deleted_uid': uid, 'deleted_name': player_name, 'deleted_gid': group['key'], 'deleted_last_online': last_online, 'kept_uid': uid, 'kept_name': prev_name, 'kept_gid': prev_group['key'], 'kept_last_online': prev_lo})
                    continue
                else:
                    prev_group['value']['RawData']['value']['players'] = [p for p in prev_group['value']['RawData']['value'].get('players', []) if str(p.get('player_uid', '')).replace('-', '') != uid]
                    deleted_players.append({'deleted_uid': uid, 'deleted_name': prev_name, 'deleted_gid': prev_group['key'], 'deleted_last_online': prev_lo, 'kept_uid': uid, 'kept_name': player_name, 'kept_gid': group['key'], 'kept_last_online': last_online})
            uid_to_player[uid] = player
            uid_to_group[uid] = group
            filtered_players.append(player)
        raw['players'] = filtered_players
    deleted_uids = {d['deleted_uid'] for d in deleted_players}
    if deleted_uids:
        constants.files_to_delete.update(deleted_uids)
        delete_player_pals(wsd, deleted_uids)
    valid_uids = {str(p.get('player_uid', '')).replace('-', '') for g in wsd['GroupSaveDataMap']['value'] if g['value']['GroupType']['value']['value'] == 'EPalGroupType::Guild' for p in g['value']['RawData']['value'].get('players', [])}
    clean_character_save_parameter_map(wsd, valid_uids)
    return len(deleted_players)
def delete_unreferenced_data(parent=None):
    if not constants.loaded_level_json:
        return {}
    build_player_levels()
    def normalize_uid(uid):
        if isinstance(uid, dict):
            uid = uid.get('value', '')
        return str(uid).replace('-', '').lower()
    def is_broken_mapobject(obj):
        bp = obj.get('Model', {}).get('value', {}).get('BuildProcess', {}).get('value', {}).get('RawData', {}).get('value', {})
        return bp.get('state') == 0
    def is_dropped_item(obj):
        return obj.get('ConcreteModel', {}).get('value', {}).get('RawData', {}).get('value', {}).get('concrete_model_type') == 'PalMapObjectDropItemModel'
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    group_data_list = wsd.get('GroupSaveDataMap', {}).get('value', [])
    char_map = wsd.get('CharacterSaveParameterMap', {}).get('value', [])
    char_containers = wsd.get('CharacterContainerSaveData', {}).get('value', [])
    valid_container_ids = set()
    for cont in char_containers:
        try:
            cont_id = normalize_uid(cont.get('key', {}).get('ID', {}).get('value', ''))
            if cont_id:
                valid_container_ids.add(cont_id)
        except:
            pass
    char_uids = set()
    for entry in char_map:
        uid_raw = entry.get('key', {}).get('PlayerUId')
        uid = normalize_uid(uid_raw)
        owner_uid_raw = entry.get('value', {}).get('RawData', {}).get('value', {}).get('object', {}).get('SaveParameter', {}).get('value', {}).get('OwnerPlayerUId')
        owner_uid = normalize_uid(owner_uid_raw)
        if uid:
            char_uids.add(uid)
        if owner_uid:
            char_uids.add(owner_uid)
    unreferenced_uids, invalid_uids, removed_guilds = ([], [], 0)
    deleted_guild_ids = []
    for group in group_data_list[:]:
        if group['value']['GroupType']['value']['value'] != 'EPalGroupType::Guild':
            continue
        raw = group['value']['RawData']['value']
        players = raw.get('players', [])
        valid_players = []
        all_invalid = True
        for p in players:
            pid_raw = p.get('player_uid')
            pid = normalize_uid(pid_raw)
            if pid not in char_uids:
                unreferenced_uids.append(pid)
                continue
            level = constants.player_levels.get(pid, None)
            if is_valid_level(level):
                all_invalid = False
                valid_players.append(p)
            else:
                invalid_uids.append(pid)
        if not valid_players or all_invalid:
            gid_raw = group['key']
            gid = normalize_uid(gid_raw)
            deleted_guild_ids.append(gid)
            for b in wsd.get('BaseCampSaveData', {}).get('value', [])[:]:
                base_gid_raw = b['value']['RawData']['value'].get('group_id_belong_to')
                base_gid = normalize_uid(base_gid_raw)
                if base_gid == gid:
                    delete_base_camp(b, gid_raw, delete_workers=True)
            group_data_list.remove(group)
            removed_guilds += 1
            continue
        raw['players'] = valid_players
        admin_uid_raw = raw.get('admin_player_uid')
        admin_uid = normalize_uid(admin_uid_raw)
        keep_uids = {normalize_uid(p.get('player_uid')) for p in valid_players}
        if admin_uid not in keep_uids:
            raw['admin_player_uid'] = valid_players[0]['player_uid']
    orphaned_pals = []
    for entry in char_map[:]:
        try:
            raw = entry['value']['RawData']['value']
            sp = raw['object']['SaveParameter']['value']
            if sp.get('IsPlayer', {}).get('value'):
                continue
            owner_uid = normalize_uid(sp.get('OwnerPlayerUId', ''))
            if owner_uid and owner_uid != '000000000000000000000000000000000':
                continue
            slot_id_obj = sp.get('SlotId', {}).get('value', {}).get('ContainerId', {}).get('value', {}).get('ID', {})
            slot_id = normalize_uid(slot_id_obj.get('value', slot_id_obj) if isinstance(slot_id_obj, dict) else slot_id_obj)
            if slot_id and slot_id != '000000000000000000000000000000000' and (slot_id not in valid_container_ids):
                orphaned_pals.append(entry)
        except:
            pass
    for pal in orphaned_pals:
        if pal in char_map:
            char_map.remove(pal)
    char_map[:] = [entry for entry in char_map if normalize_uid(entry.get('key', {}).get('PlayerUId')) not in unreferenced_uids + invalid_uids and normalize_uid(entry.get('value', {}).get('RawData', {}).get('value', {}).get('object', {}).get('SaveParameter', {}).get('value', {}).get('OwnerPlayerUId')) not in unreferenced_uids + invalid_uids]
    all_removed_uids = set(unreferenced_uids + invalid_uids)
    constants.files_to_delete.update(all_removed_uids)
    removed_pals = delete_player_pals(wsd, all_removed_uids)
    if all_removed_uids:
        map_objs = wsd.get('MapObjectSaveData', {}).get('value', {}).get('values', [])
        for obj in map_objs:
            try:
                raw = obj.get('Model', {}).get('value', {}).get('RawData', {}).get('value', {})
                build_uid = raw.get('build_player_uid')
                if build_uid and normalize_uid(build_uid) in all_removed_uids:
                    raw['build_player_uid'] = '00000000-0000-0000-0000-000000000000'
                stage_id = raw.get('stage_instance_id_belong_to', {})
                if isinstance(stage_id, dict):
                    stage_guid = stage_id.get('id')
                    if stage_guid and normalize_uid(stage_guid) in all_removed_uids:
                        stage_id['id'] = '00000000-0000-0000-0000-000000000000'
            except:
                pass
        char_containers = wsd.get('CharacterContainerSaveData', {}).get('value', [])
        for cont in char_containers:
            try:
                slots = cont['value']['Slots']['value']['values']
                for slot in slots:
                    player_uid = slot.get('RawData', {}).get('value', {}).get('player_uid')
                    if player_uid and normalize_uid(player_uid) in all_removed_uids:
                        slot['RawData']['value']['player_uid'] = '00000000-0000-0000-0000-000000000000'
            except:
                pass
        group_map = wsd.get('GroupSaveDataMap', {}).get('value', [])
        for g in group_map:
            try:
                raw = g['value']['RawData']['value']
                handle_ids = raw.get('individual_character_handle_ids', [])
                if handle_ids:
                    cleaned_handles = []
                    for h in handle_ids:
                        if isinstance(h, dict):
                            guid = normalize_uid(h.get('guid', ''))
                            if guid not in all_removed_uids:
                                cleaned_handles.append(h)
                        else:
                            cleaned_handles.append(h)
                    raw['individual_character_handle_ids'] = cleaned_handles
            except:
                pass
    if deleted_guild_ids:
        guild_extra_map = wsd.get('GuildExtraSaveDataMap', {}).get('value', [])
        guild_extra_map[:] = [entry for entry in guild_extra_map if normalize_uid(entry.get('key', '')) not in deleted_guild_ids]
    map_objects_wrapper = wsd.get('MapObjectSaveData', {}).get('value', {})
    map_objects = map_objects_wrapper.get('values', [])
    broken_ids, dropped_ids = ([], [])
    new_map_objects = []
    for obj in map_objects:
        if is_broken_mapobject(obj):
            instance_id = obj.get('Model', {}).get('value', {}).get('RawData', {}).get('value', {}).get('instance_id')
            broken_ids.append(instance_id)
        elif is_dropped_item(obj):
            instance_id = obj.get('ConcreteModel', {}).get('value', {}).get('RawData', {}).get('value', {}).get('instance_id')
            dropped_ids.append(instance_id)
        else:
            new_map_objects.append(obj)
    map_objects_wrapper['values'] = new_map_objects
    removed_broken, removed_drops = (len(broken_ids), len(dropped_ids))
    removed_orphaned_dynamic = delete_orphaned_dynamic_items()
    return {'characters': len(all_removed_uids), 'pals': removed_pals + len(orphaned_pals), 'guilds': removed_guilds, 'broken_objects': removed_broken, 'dropped_items': removed_drops, 'orphaned_dynamic_items': removed_orphaned_dynamic}
def delete_non_base_map_objects(parent=None):
    if not constants.loaded_level_json:
        return 0
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    base_camp_list = wsd['BaseCampSaveData']['value']
    active_base_ids = {b['key'] for b in base_camp_list}
    map_objs = wsd['MapObjectSaveData']['value']['values']
    initial_count = len(map_objs)
    new_map_objs = []
    for m in map_objs:
        raw_data = m.get('Model', {}).get('value', {}).get('RawData', {}).get('value', {})
        base_camp_id = raw_data.get('base_camp_id_belong_to')
        instance_id = raw_data.get('instance_id', 'UNKNOWN_ID')
        object_name = m.get('MapObjectId', {}).get('value', 'UNKNOWN_OBJECT_TYPE')
        should_keep = False
        if base_camp_id and base_camp_id in active_base_ids:
            should_keep = True
        if should_keep:
            new_map_objs.append(m)
        else:
            pass
    deleted_count = initial_count - len(new_map_objs)
    map_objs[:] = new_map_objs
    return deleted_count
def delete_invalid_structure_map_objects(parent=None):
    if not constants.loaded_level_json:
        return 0
    import json, os
    valid_assets = set()
    try:
        base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        fp = os.path.join(base_dir, 'resources', 'game_data', 'structuredata.json')
        with open(fp, 'r', encoding='utf-8') as f:
            js = json.load(f)
            for x in js.get('structures', []):
                asset = x.get('asset')
                if isinstance(asset, str):
                    valid_assets.add(asset.lower())
    except Exception as e:
        return 0
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    map_objs = wsd['MapObjectSaveData']['value']['values']
    initial_count = len(map_objs)
    new_map_objs = []
    for m in map_objs:
        object_id_node = m.get('MapObjectId', {})
        object_name = object_id_node.get('value')
        if isinstance(object_name, str) and object_name.lower() in valid_assets:
            new_map_objs.append(m)
        else:
            pass
    deleted_count = initial_count - len(new_map_objs)
    map_objs[:] = new_map_objs
    return deleted_count
def delete_all_skins(parent=None):
    if not constants.loaded_level_json:
        return 0
    removed_level_skins = 0
    def clean_level_skins(data):
        nonlocal removed_level_skins
        if isinstance(data, dict):
            if 'SkinName' in data:
                del data['SkinName']
                removed_level_skins += 1
            if 'SkinAppliedCharacterId' in data:
                del data['SkinAppliedCharacterId']
            for v in list(data.values()):
                clean_level_skins(v)
        elif isinstance(data, list):
            for item in data:
                clean_level_skins(item)
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        clean_level_skins(wsd)
    except:
        pass
    players_dir = os.path.join(constants.current_save_path, 'Players')
    fixed_player_files = 0
    if os.path.exists(players_dir):
        player_files = [f for f in os.listdir(players_dir) if f.endswith('.sav') and '_dps' not in f]
        if player_files:
            def process_player_file(filename):
                file_path = os.path.join(players_dir, filename)
                try:
                    gvas = sav_to_gvasfile(file_path)
                    changed = False
                    def remove_skin_info(data):
                        nonlocal changed
                        if isinstance(data, dict):
                            if 'SkinInventoryInfo' in data:
                                del data['SkinInventoryInfo']
                                changed = True
                            for v in list(data.values()):
                                remove_skin_info(v)
                        elif isinstance(data, list):
                            for item in data:
                                remove_skin_info(item)
                    remove_skin_info(gvas.properties)
                    if changed:
                        gvasfile_to_sav(gvas, file_path)
                        return 1
                except:
                    pass
                return 0
            with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
                results = list(executor.map(process_player_file, player_files))
                fixed_player_files = sum(results)
    return removed_level_skins + fixed_player_files
def unlock_all_private_chests(parent=None):
    if not constants.loaded_level_json:
        return 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    except KeyError:
        return 0
    count = 0
    def deep_unlock(data):
        nonlocal count
        if isinstance(data, dict):
            ctype = data.get('concrete_model_type', '')
            if ctype in ('PalMapObjectItemBoothModel', 'PalMapObjectPalBoothModel'):
                return
            if 'private_lock_player_uid' in data:
                data['private_lock_player_uid'] = '00000000-0000-0000-0000-000000000000'
                count += 1
            for v in data.values():
                deep_unlock(v)
        elif isinstance(data, list):
            for item in data:
                deep_unlock(item)
    deep_unlock(wsd)
    return count
def remove_invalid_items_from_level(parent=None):
    if not constants.loaded_level_json:
        return 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    except:
        return 0
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    valid_items = set()
    try:
        fp = os.path.join(base_dir, 'resources', 'game_data', 'itemdata.json')
        with open(fp, 'r', encoding='utf-8') as f:
            js = json.load(f)
            for x in js.get('items', []):
                aid = x.get('asset')
                if isinstance(aid, str):
                    valid_items.add(aid.lower())
    except:
        pass
    removed_count = 0
    def clean_recursive(data):
        nonlocal removed_count
        if isinstance(data, dict):
            for key in list(data.keys()):
                val = data[key]
                if isinstance(val, (dict, list)):
                    clean_recursive(val)
        elif isinstance(data, list):
            i = len(data) - 1
            while i >= 0:
                item_obj = data[i]
                if isinstance(item_obj, dict) and 'RawData' in item_obj:
                    raw_val = item_obj['RawData'].get('value', {})
                    sid = None
                    if isinstance(raw_val, dict):
                        if 'item' in raw_val and isinstance(raw_val['item'], dict):
                            sid = raw_val['item'].get('static_id')
                        elif 'id' in raw_val and isinstance(raw_val['id'], dict):
                            sid = raw_val['id'].get('static_id')
                    if isinstance(sid, str) and sid.lower() not in valid_items:
                        data.pop(i)
                        removed_count += 1
                    else:
                        clean_recursive(item_obj)
                else:
                    clean_recursive(item_obj)
                i -= 1
    clean_recursive(wsd)
    return removed_count
def remove_invalid_items_from_save(parent=None):
    if not constants.current_save_path:
        return 0
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    valid_items = set()
    try:
        fp = os.path.join(base_dir, 'resources', 'game_data', 'itemdata.json')
        with open(fp, 'r', encoding='utf-8') as f:
            js = json.load(f)
            for x in js.get('items', []):
                aid = x.get('asset')
                if isinstance(aid, str):
                    valid_items.add(aid.lower())
    except:
        pass
    players_dir = os.path.join(constants.current_save_path, 'Players')
    if not os.path.exists(players_dir):
        return 0
    total_files = 0
    fixed_files = 0
    total_removed = 0
    def clean_craft_records(data, filename):
        nonlocal total_removed
        changed = False
        if isinstance(data, dict):
            if 'CraftItemCount' in data and isinstance(data['CraftItemCount'].get('value'), list):
                old_list = data['CraftItemCount']['value']
                new_list = []
                for i in old_list:
                    key = i.get('key')
                    if isinstance(key, str) and key.lower() in valid_items:
                        new_list.append(i)
                    else:
                        changed = True
                        total_removed += 1
                if changed:
                    data['CraftItemCount']['value'] = new_list
            for v in data.values():
                if clean_craft_records(v, filename):
                    changed = True
        elif isinstance(data, list):
            for item in data:
                if clean_craft_records(item, filename):
                    changed = True
        return changed
    player_files = [f for f in os.listdir(players_dir) if f.endswith('.sav') and '_dps' not in f]
    total_files = len(player_files)
    if player_files:
        def process_player_file(filename):
            file_path = os.path.join(players_dir, filename)
            try:
                gvas = sav_to_gvasfile(file_path)
                if clean_craft_records(gvas.properties, filename):
                    gvasfile_to_sav(gvas, file_path)
                    return 1
            except Exception as e:
                pass
            return 0
        with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
            results = list(executor.map(process_player_file, player_files))
            fixed_files = sum(results)
    remove_invalid_items_from_level(parent)
    return fixed_files
def remove_invalid_pals_from_save(parent=None):
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    def load_assets(fname, key):
        try:
            fp = os.path.join(base_dir, 'resources', 'game_data', fname)
            with open(fp, 'r', encoding='utf-8') as f:
                data = json.load(f)
                return set((x['asset'].lower() for x in data.get(key, [])))
        except:
            return set()
    valid_pals = load_assets('paldata.json', 'pals')
    valid_npcs = load_assets('npcdata.json', 'npcs')
    valid_all = valid_pals | valid_npcs
    if not constants.current_save_path or not constants.loaded_level_json:
        return 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    except:
        return 0
    cmap = wsd.get('CharacterSaveParameterMap', {}).get('value', [])
    removed_ids = set()
    removed = 0
    def get_char_id(e):
        try:
            return e['value']['RawData']['value']['object']['SaveParameter']['value']['CharacterID']['value']
        except:
            return None
    filtered = []
    for entry in cmap:
        cid = get_char_id(entry)
        if cid and cid.lower() not in valid_all:
            inst = str(entry['key']['InstanceId']['value'])
            removed_ids.add(inst)
            removed += 1
            continue
        filtered.append(entry)
    wsd['CharacterSaveParameterMap']['value'] = filtered
    containers = wsd.get('CharacterContainerSaveData', {}).get('value', [])
    for cont in containers:
        try:
            slots = cont['value']['Slots']['value']['values']
        except:
            continue
        newslots = []
        for s in slots:
            inst = s.get('RawData', {}).get('value', {}).get('instance_id')
            if inst and str(inst) in removed_ids:
                continue
            newslots.append(s)
        cont['value']['Slots']['value']['values'] = newslots
    return removed
def fix_missions(parent=None):
    if not constants.current_save_path:
        return {'total': 0, 'fixed': 0, 'skipped': 0}
    save_path = os.path.join(constants.current_save_path, 'Players')
    if not os.path.exists(save_path):
        return {'total': 0, 'fixed': 0, 'skipped': 0}
    player_files = [f for f in os.listdir(save_path) if f.endswith('.sav') and '_dps' not in f]
    if not player_files:
        return {'total': 0, 'fixed': 0, 'skipped': 0}
    def deep_delete_completed_quest_array(data):
        found = False
        if isinstance(data, dict):
            if 'CompletedQuestArray' in data:
                data['CompletedQuestArray']['value']['values'] = []
                return True
            for v in data.values():
                if deep_delete_completed_quest_array(v):
                    found = True
        elif isinstance(data, list):
            for item in data:
                if deep_delete_completed_quest_array(item):
                    found = True
        return found
    def process_player_file(filename):
        file_path = os.path.join(save_path, filename)
        try:
            gvas = sav_to_gvasfile(file_path)
            if deep_delete_completed_quest_array(gvas.properties):
                gvasfile_to_sav(gvas, file_path)
                return (1, 1, 0)
            else:
                return (1, 0, 0)
        except Exception as e:
            return (1, 0, 1)
    with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
        results = list(executor.map(process_player_file, player_files))
    total = sum((r[0] for r in results))
    fixed = sum((r[1] for r in results))
    skipped = sum((r[2] for r in results))
    return {'total': total, 'fixed': fixed, 'skipped': skipped}
def reset_anti_air_turrets(parent=None):
    if not constants.loaded_level_json:
        return None
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    except KeyError:
        return None
    if 'FixedWeaponDestroySaveData' in wsd:
        data = wsd['FixedWeaponDestroySaveData']
        count = 0
        if isinstance(data, dict):
            values = data.get('value', [])
            if isinstance(values, list):
                count = len(values)
            elif isinstance(values, dict):
                count = len(values.get('values', []))
        del wsd['FixedWeaponDestroySaveData']
        return count if count > 0 else 1
    return 0
def reset_dungeons(parent=None):
    if not constants.loaded_level_json:
        return None
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    except KeyError:
        return None
    if 'DungeonPointMarkerSaveData' in wsd:
        data = wsd['DungeonPointMarkerSaveData']
        count = 0
        if isinstance(data, dict):
            values = data.get('value', [])
            if isinstance(values, list):
                count = len(values)
            elif isinstance(values, dict):
                count = len(values.get('values', []))
        del wsd['DungeonPointMarkerSaveData']
        return count if count > 0 else 1
    return 0
def unlock_viewing_cage_for_player(player_uid, parent=None):
    if not constants.current_save_path:
        return False
    player_id = str(player_uid).replace('-', '').lower()
    file_path = os.path.join(constants.current_save_path, 'Players', f'{player_id.zfill(32)}.sav')
    if not os.path.exists(file_path):
        return False
    try:
        gvas = sav_to_gvasfile(file_path)
        changed = False
        def inject_viewing_cage(data):
            nonlocal changed
            if isinstance(data, dict):
                if 'UnlockedRecipeTechnologyNames' in data:
                    values_list = data['UnlockedRecipeTechnologyNames']['value']['values']
                    if 'DisplayCharacter' not in values_list:
                        values_list.append('DisplayCharacter')
                        changed = True
                for v in data.values():
                    inject_viewing_cage(v)
            elif isinstance(data, list):
                for item in data:
                    inject_viewing_cage(item)
        inject_viewing_cage(gvas.properties)
        if changed:
            gvasfile_to_sav(gvas, file_path)
            return True
        return False
    except Exception as e:
        return False
def detect_and_trim_overfilled_inventories(parent=None):
    import copy
    if not constants.current_save_path:
        return 0
    players_dir = os.path.join(constants.current_save_path, 'Players')
    if not os.path.exists(players_dir):
        return 0
    player_files = [f for f in os.listdir(players_dir) if f.endswith('.sav') and '_dps' not in f]
    fixed_containers = 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        item_containers = wsd.get('ItemContainerSaveData', {}).get('value', [])
        container_lookup = {str(c['key']['ID']['value']): c for c in item_containers if 'key' in c}
        for player_file in player_files:
            player_uid = player_file.replace('.sav', '')
            try:
                player_path = os.path.join(players_dir, player_file)
                player_gvas = sav_to_gvasfile(player_path)
                player_props = player_gvas.properties
                if 'properties' in player_props and 'SaveData' in player_props['properties']:
                    inv_info = player_props['properties']['SaveData']['value']['InventoryInfo']['value']
                elif 'SaveData' in player_props:
                    inv_info = player_props['SaveData']['value']['InventoryInfo']['value']
                else:
                    continue
                main_id = str(inv_info['CommonContainerId']['value']['ID']['value'])
                key_id = str(inv_info['EssentialContainerId']['value']['ID']['value'])
                additional_inventory_count = 0
                if key_id in container_lookup:
                    key_slots = container_lookup[key_id]['value']['Slots']['value']['values']
                    additional_items = ['AdditionalInventory_001', 'AdditionalInventory_002', 'AdditionalInventory_003', 'AdditionalInventory_004']
                    for slot in key_slots:
                        try:
                            item_id = slot.get('RawData', {}).get('value', {}).get('item', {}).get('static_id', '')
                            if item_id in additional_items:
                                additional_inventory_count += 1
                        except:
                            continue
                player_max_slots = 42 + additional_inventory_count * 3
                if main_id in container_lookup:
                    container = container_lookup[main_id]
                    slots = container['value']['Slots']['value']['values']
                    current_slot_num = container['value'].get('SlotNum', {}).get('value', 0)
                    if len(slots) != player_max_slots or current_slot_num != player_max_slots:
                        if len(slots) >= player_max_slots or (len(slots) < player_max_slots and len(slots) >= 42):
                            if len(slots) > player_max_slots:
                                slots[:] = slots[:player_max_slots]
                            elif len(slots) < player_max_slots:
                                if len(slots) > 0:
                                    template_slot = copy.deepcopy(slots[0])
                                    template_slot['RawData']['value']['item']['static_id'] = ''
                                    template_slot['RawData']['value']['item']['dynamic_id']['created_world_id'] = '00000000-0000-0000-0000-000000000000'
                                    template_slot['RawData']['value']['item']['dynamic_id']['local_id'] = '00000000-0000-0000-0000-000000000000'
                                    template_slot['RawData']['value']['count'] = 0
                                    while len(slots) < player_max_slots:
                                        slots.append(copy.deepcopy(template_slot))
                            if 'SlotNum' in container['value']:
                                container['value']['SlotNum']['value'] = len(slots)
                            fixed_containers += 1
            except Exception as e:
                pass
        return fixed_containers
    except Exception as e:
        return 0
def fix_all_negative_timestamps(parent=None):
    if not constants.loaded_level_json:
        return 0
    fixed_count = 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        if 'GameTimeSaveData' not in wsd:
            return 0
        current_tick = int(wsd['GameTimeSaveData']['value']['RealDateTimeTicks']['value'])
        if 'CharacterSaveParameterMap' in wsd:
            for char in wsd['CharacterSaveParameterMap']['value']:
                try:
                    raw = char['value']['RawData']['value']
                    if 'last_online_real_time' in raw:
                        last_time = raw.get('last_online_real_time')
                        if last_time and int(last_time) > current_tick:
                            raw['last_online_real_time'] = current_tick
                            fixed_count += 1
                    if 'object' in raw and 'SaveParameter' in raw['object']:
                        p = raw['object']['SaveParameter']['value']
                        if 'LastOnlineRealTime' in p:
                            last_time = p['LastOnlineRealTime'].get('value')
                            if last_time and int(last_time) > current_tick:
                                p['LastOnlineRealTime']['value'] = current_tick
                                fixed_count += 1
                except:
                    continue
        if 'GroupSaveDataMap' in wsd:
            group_map = wsd['GroupSaveDataMap']['value']
            for gdata in group_map:
                try:
                    if gdata['value']['GroupType']['value']['value'] != 'EPalGroupType::Guild':
                        continue
                    players = gdata['value']['RawData']['value'].get('players', [])
                    for p_info in players:
                        if 'player_info' in p_info and 'last_online_real_time' in p_info['player_info']:
                            last_time = p_info['player_info'].get('last_online_real_time')
                            if last_time and int(last_time) > current_tick:
                                p_info['player_info']['last_online_real_time'] = current_tick
                                fixed_count += 1
                except:
                    continue
    except Exception as e:
        pass
    return fixed_count
def reset_selected_player_timestamp(player_uid, parent=None):
    if not constants.loaded_level_json:
        return False
    try:
        uid_clean = str(player_uid).replace('-', '').lower()
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        current_tick = int(wsd['GameTimeSaveData']['value']['RealDateTimeTicks']['value'])
        if 'CharacterSaveParameterMap' in wsd:
            for char in wsd['CharacterSaveParameterMap']['value']:
                char_uid = str(char['key']['PlayerUId']['value']).replace('-', '').lower()
                if char_uid == uid_clean:
                    raw = char['value']['RawData']['value']
                    raw['last_online_real_time'] = current_tick
                    if 'object' in raw and 'SaveParameter' in raw['object']:
                        p = raw['object']['SaveParameter']['value']
                        if 'LastOnlineRealTime' in p:
                            p['LastOnlineRealTime']['value'] = current_tick
        if 'GroupSaveDataMap' in wsd:
            group_map = wsd['GroupSaveDataMap']['value']
            items = group_map.items() if isinstance(group_map, dict) else enumerate(group_map)
            for _, gdata in items:
                players = gdata['value']['RawData']['value'].get('players', [])
                for p_info in players:
                    if str(p_info.get('player_uid', '')).replace('-', '').lower() == uid_clean:
                        if 'player_info' in p_info:
                            p_info['player_info']['last_online_real_time'] = current_tick
        return True
    except Exception as e:
        return False
def remove_invalid_passives_from_save(parent=None):
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    valid_passives = set()
    try:
        fp = os.path.join(base_dir, 'resources', 'game_data', 'passivedata.json')
        with open(fp, 'r', encoding='utf-8') as f:
            js = json.load(f)
            for x in js.get('passives', []):
                asset = x.get('asset')
                if isinstance(asset, str):
                    valid_passives.add(asset.lower())
    except:
        return 0
    if not constants.current_save_path or not constants.loaded_level_json:
        return 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    except:
        return 0
    players_dir = os.path.join(constants.current_save_path, 'Players')
    removed_count = 0
    cmap = wsd.get('CharacterSaveParameterMap', {}).get('value', [])
    for itm in cmap:
        try:
            raw = itm['value']['RawData']['value']['object']['SaveParameter']['value']
            if 'PassiveSkillList' in raw:
                p_list = raw['PassiveSkillList']['value']['values']
                new_p_list = [s for s in p_list if s.lower() in valid_passives]
                removed = len(p_list) - len(new_p_list)
                if removed > 0:
                    raw['PassiveSkillList']['value']['values'] = new_p_list
                    removed_count += removed
        except:
            pass
    if os.path.exists(players_dir):
        player_files = [f for f in os.listdir(players_dir) if f.endswith('.sav') and '_dps' not in f]
        if player_files:
            def process_player_file(filename):
                file_path = os.path.join(players_dir, filename)
                local_removed = 0
                try:
                    gvas = sav_to_gvasfile(file_path)
                    changed = False
                    def remove_invalid_passives(data):
                        nonlocal changed, local_removed
                        if isinstance(data, dict):
                            if 'PassiveSkills' in data and isinstance(data['PassiveSkills'], dict):
                                skills = data['PassiveSkills'].get('value', [])
                                if isinstance(skills, list):
                                    new_skills = []
                                    for skill in skills:
                                        skill_name = skill.get('value', '').lower()
                                        if skill_name in valid_passives:
                                            new_skills.append(skill)
                                        else:
                                            changed = True
                                            local_removed += 1
                                    if changed:
                                        data['PassiveSkills']['value'] = new_skills
                            for v in data.values():
                                remove_invalid_passives(v)
                        elif isinstance(data, list):
                            for item in data:
                                remove_invalid_passives(item)
                    remove_invalid_passives(gvas.properties)
                    if changed:
                        gvasfile_to_sav(gvas, file_path)
                except:
                    pass
                return local_removed
            with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
                results = list(executor.map(process_player_file, player_files))
                removed_count += sum(results)
    return removed_count
def unlock_all_technologies_for_player(player_uid, parent=None):
    if not constants.current_save_path:
        return False
    player_id = str(player_uid).replace('-', '').lower()
    file_path = os.path.join(constants.current_save_path, 'Players', f'{player_id.zfill(32)}.sav')
    if not os.path.exists(file_path):
        return False
    try:
        base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        tech_file = os.path.join(base_dir, 'resources', 'game_data', 'technologydata.json')
        with open(tech_file, 'r', encoding='utf-8') as f:
            tech_data = json.load(f)
        all_techs = [item['asset'] for item in tech_data.get('technology', [])]
        gvas = sav_to_gvasfile(file_path)
        def inject_all_techs(data):
            if isinstance(data, dict):
                if 'UnlockedRecipeTechnologyNames' in data:
                    values_list = data['UnlockedRecipeTechnologyNames']['value']['values']
                    current_set = set(values_list)
                    for tech in all_techs:
                        if tech not in current_set:
                            values_list.append(tech)
                for v in data.values():
                    inject_all_techs(v)
            elif isinstance(data, list):
                for item in data:
                    inject_all_techs(item)
        inject_all_techs(gvas.properties)
        gvasfile_to_sav(gvas, file_path)
        return True
    except Exception as e:
        return False
def unlock_all_lab_research_for_guild(guild_id, parent=None):
    if not constants.loaded_level_json:
        return False
    try:
        base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        research_file = os.path.join(base_dir, 'resources', 'game_data', 'labresearchdata.json')
        with open(research_file, 'r', encoding='utf-8') as f:
            research_data = json.load(f)
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        group_data = wsd.get('GroupSaveDataMap', {}).get('value', [])
        target_guild = None
        for g in group_data:
            if g['value']['GroupType']['value']['value'] == 'EPalGroupType::Guild':
                gid = str(g['key']).replace('-', '').lower()
                if gid == str(guild_id).replace('-', '').lower():
                    target_guild = g
                    break
        if not target_guild:
            return False
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        if 'GuildExtraSaveDataMap' not in wsd:
            return False
        guild_extra_map = wsd['GuildExtraSaveDataMap'].get('value', [])
        target_extra = None
        for extra_entry in guild_extra_map:
            if isinstance(extra_entry, dict) and 'key' in extra_entry:
                extra_gid = str(extra_entry['key']).replace('-', '').lower()
                search_gid = str(guild_id).replace('-', '').lower()
                if extra_gid == search_gid:
                    target_extra = extra_entry
                    break
        if not target_extra:
            return False
        extra_value = target_extra.get('value', {})
        if 'Lab' not in extra_value:
            return False
        lab_data = extra_value['Lab']['value']['RawData']['value']
        complete_research_list = []
        for research_id, research_info in research_data.items():
            complete_research_list.append({'research_id': research_id, 'work_amount': research_info['work_amount']})
        lab_data['research_info'] = complete_research_list
        return True
    except Exception as e:
        return False
def modify_container_slots(new_slot_num, parent=None):
    if not constants.loaded_level_json:
        return 0
    try:
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        modified = 0
        import copy
        map_objects = wsd.get('MapObjectSaveData', {}).get('value', {}).get('values', [])
        valid_container_ids = set()
        for obj in map_objects:
            map_object_id = obj.get('MapObjectId', {}).get('value')
            if map_object_id and ('ItemChest' in map_object_id or 'GuildChest' in map_object_id):
                bp = obj.get('Model', {}).get('value', {}).get('BuildProcess', {}).get('value', {}).get('RawData', {}).get('value', {})
                if bp.get('state') == 1:
                    raw_data = obj.get('Model', {}).get('value', {}).get('RawData', {}).get('value', {})
                    base_camp_id = raw_data.get('base_camp_id_belong_to')
                    group_id = raw_data.get('group_id_belong_to')
                    if base_camp_id and base_camp_id != '00000000-0000-0000-0000-000000000000' and group_id and (group_id != '00000000-0000-0000-0000-000000000000'):
                        module_map = obj.get('ConcreteModel', {}).get('value', {}).get('ModuleMap', {}).get('value', [])
                        for module in module_map:
                            if module.get('key') == 'EPalMapObjectConcreteModelModuleType::ItemContainer':
                                module_raw = module.get('value', {}).get('RawData', {}).get('value', {})
                                target_id = module_raw.get('target_container_id')
                                if target_id:
                                    valid_container_ids.add(str(target_id))
                                break
        item_containers = wsd.get('ItemContainerSaveData', {}).get('value', [])
        for cont in item_containers:
            try:
                container_id = str(cont['key']['ID']['value'])
                if container_id not in valid_container_ids:
                    continue
                value = cont['value']
                current_slot_num = value.get('SlotNum', {}).get('value', 0)
                linked = False
                map_object_id = 'Unknown'
                for obj in map_objects:
                    module_map = obj.get('ConcreteModel', {}).get('value', {}).get('ModuleMap', {}).get('value', [])
                    for module in module_map:
                        if module.get('key') == 'EPalMapObjectConcreteModelModuleType::ItemContainer':
                            module_raw = module.get('value', {}).get('RawData', {}).get('value', {})
                            if str(module_raw.get('target_container_id')) == container_id:
                                map_object_id = obj.get('MapObjectId', {}).get('value', 'Unknown')
                                linked = True
                                break
                    if linked:
                        break
                if not linked:
                    continue
                slots = value.get('Slots', {}).get('value', {}).get('values', [])
                if current_slot_num == new_slot_num:
                    continue
                if len(slots) < new_slot_num:
                    if slots:
                        template = copy.deepcopy(slots[0])
                        template['RawData']['value']['item']['static_id'] = ''
                        template['RawData']['value']['item']['dynamic_id']['created_world_id'] = '00000000-0000-0000-0000-000000000000'
                        template['RawData']['value']['item']['dynamic_id']['local_id'] = '00000000-0000-0000-0000-000000000000'
                        template['RawData']['value']['count'] = 0
                        while len(slots) < new_slot_num:
                            slots.append(copy.deepcopy(template))
                    else:
                        pass
                elif len(slots) > new_slot_num:
                    slots[:] = slots[:new_slot_num]
                if 'SlotNum' in value:
                    value['SlotNum']['value'] = new_slot_num
                    modified += 1
            except:
                pass
        return modified
    except:
        return 0
def delete_orphaned_dynamic_items(parent=None):
    if not constants.loaded_level_json:
        return 0
    def normalize_uid(uid):
        if isinstance(uid, dict):
            uid = uid.get('value', '')
        return str(uid).replace('-', '').lower()
    wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
    dynamic_items = wsd.get('DynamicItemSaveData', {}).get('value', {}).get('values', [])
    dynamic_ids = set()
    for di in dynamic_items:
        try:
            lid = di.get('RawData', {}).get('value', {}).get('id', {}).get('local_id_in_created_world', '')
            if lid and lid != '00000000-0000-0000-0000-000000000000':
                dynamic_ids.add(normalize_uid(lid))
        except:
            pass
    if not dynamic_ids:
        return 0
    referenced_dynamic_ids = set()
    def scan_container(cont):
        try:
            slots = cont.get('value', {}).get('Slots', {}).get('value', {}).get('values', [])
            for slot in slots:
                raw = slot.get('RawData', {}).get('value', {})
                item = raw.get('item', {})
                if item and isinstance(item, dict):
                    dynamic_id = item.get('dynamic_id', {})
                    if isinstance(dynamic_id, dict):
                        lid = dynamic_id.get('local_id_in_created_world', '')
                        if lid:
                            normalized = normalize_uid(lid)
                            if normalized in dynamic_ids:
                                referenced_dynamic_ids.add(normalized)
                items = raw.get('items', {}).get('value', {}).get('values', [])
                for it in items:
                    it_raw = it.get('RawData', {})
                    if it_raw and isinstance(it_raw, dict):
                        dynamic_id = it_raw.get('dynamic_id', {})
                        if isinstance(dynamic_id, dict):
                            lid = dynamic_id.get('local_id_in_created_world', '')
                            if lid:
                                normalized = normalize_uid(lid)
                                if normalized in dynamic_ids:
                                    referenced_dynamic_ids.add(normalized)
        except:
            pass
    for cont in wsd.get('ItemContainerSaveData', {}).get('value', []):
        scan_container(cont)
    for cont in wsd.get('CharacterContainerSaveData', {}).get('value', []):
        scan_container(cont)
    orphaned_ids = dynamic_ids - referenced_dynamic_ids
    if not orphaned_ids:
        return 0
    initial_count = len(dynamic_items)
    dynamic_items[:] = [di for di in dynamic_items if normalize_uid(di.get('RawData', {}).get('value', {}).get('id', {}).get('local_id_in_created_world', '')) not in orphaned_ids]
    deleted_count = initial_count - len(dynamic_items)
    return deleted_count
def check_is_illegal_pal(raw):
    try:
        try:
            sp = raw['value']['RawData']['value']['object']['SaveParameter']['value']
        except:
            sp = raw.get('SaveParameter', {}).get('value', {})
            if not sp:
                return (False, [])
        illegal_markers = []
        level = extract_value(sp, 'Level', 1)
        if level > 65:
            illegal_markers.append('Level')
        talent_hp = extract_value(sp, 'Talent_HP', 0)
        talent_shot = extract_value(sp, 'Talent_Shot', 0)
        talent_defense = extract_value(sp, 'Talent_Defense', 0)
        if talent_hp > 100:
            illegal_markers.append('HP IV')
        if talent_shot > 100:
            illegal_markers.append('ATK IV')
        if talent_defense > 100:
            illegal_markers.append('DEF IV')
        rank_hp = extract_value(sp, 'Rank_HP', 0)
        rank_attack = extract_value(sp, 'Rank_Attack', 0)
        rank_defense = extract_value(sp, 'Rank_Defence', 0)
        rank_craftspeed = extract_value(sp, 'Rank_CraftSpeed', 0)
        if rank_hp > 20:
            illegal_markers.append('HP Soul')
        if rank_attack > 20:
            illegal_markers.append('ATK Soul')
        if rank_defense > 20:
            illegal_markers.append('DEF Soul')
        if rank_craftspeed > 20:
            illegal_markers.append('Craft Soul')
        return (len(illegal_markers) > 0, illegal_markers)
    except:
        return (False, [])
def _process_dps_file_worker(args):
    filename, players_dir, PAL_EXP_TABLE, NAMEMAP = args
    file_path = os.path.join(players_dir, filename)
    result = {'filename': filename, 'actual_pals': 0, 'illegals_fixed': 0, 'illegal_entries': [], 'changed': False, 'gvas_file': None}
    try:
        from palworld_aio.utils import sav_to_gvasfile, gvasfile_to_sav
        gvas_file = sav_to_gvasfile(file_path)
        save_param_array = gvas_file.properties.get('SaveParameterArray', {}).get('value', {}).get('values', [])
        if not save_param_array:
            return result
        actual_pals = 0
        illegals_in_file = 0
        changed = False
        illegal_entries_list = []
        player_uid_from_file = filename.replace('.sav', '').replace('_dps', '')
        for idx, entry in enumerate(save_param_array):
            sp = entry.get('SaveParameter', {}).get('value', {})
            char_id = sp.get('CharacterID', {}).get('value', 'None')
            if char_id == 'None':
                continue
            actual_pals += 1
            is_illegal, illegal_markers = check_is_illegal_pal(entry)
            if is_illegal:
                sp = entry['SaveParameter']['value']
                level = extract_value(sp, 'Level', 1)
                talent_hp = extract_value(sp, 'Talent_HP', 0)
                talent_shot = extract_value(sp, 'Talent_Shot', 0)
                talent_defense = extract_value(sp, 'Talent_Defense', 0)
                rank_hp = extract_value(sp, 'Rank_HP', 0)
                rank_attack = extract_value(sp, 'Rank_Attack', 0)
                rank_defense = extract_value(sp, 'Rank_Defence', 0)
                rank_craftspeed = extract_value(sp, 'Rank_CraftSpeed', 0)
                cid = extract_value(sp, 'CharacterID', '')
                nick = extract_value(sp, 'NickName', '')
                pal_name = NAMEMAP.get(cid.lower(), cid)
                inst_id = sp.get('InstanceId', {}).get('value', 'Unknown')
                slot_id_obj = sp.get('SlotId', {})
                if isinstance(slot_id_obj, dict):
                    slot_id_val = slot_id_obj.get('value', slot_id_obj)
                    if isinstance(slot_id_val, dict):
                        container_id_obj = slot_id_val.get('ContainerId', {})
                        if isinstance(container_id_obj, dict):
                            container_id_val = container_id_obj.get('value', container_id_obj)
                            if isinstance(container_id_val, dict):
                                container_id = container_id_val.get('ID', {}).get('value', 'Unknown')
                            else:
                                container_id = str(container_id_val) if container_id_val else 'Unknown'
                        else:
                            container_id = str(container_id_obj) if container_id_obj else 'Unknown'
                    else:
                        container_id = str(slot_id_val) if slot_id_val else 'Unknown'
                else:
                    container_id = str(slot_id_obj) if slot_id_obj else 'Unknown'
                owner_uid = extract_value(sp, 'OwnerPlayerUId', '')
                illegal_info = {'name': pal_name, 'nickname': nick, 'cid': cid, 'level': level, 'talent_hp': talent_hp, 'talent_shot': talent_shot, 'talent_defense': talent_defense, 'rank_hp': rank_hp, 'rank_attack': rank_attack, 'rank_defense': rank_defense, 'rank_craftspeed': rank_craftspeed, 'illegal_markers': illegal_markers, 'instance_id': inst_id, 'container_id': container_id, 'owner_uid': owner_uid, 'location': 'DPS Storage', 'filename': filename, 'player_uid_from_file': player_uid_from_file}
                illegal_entries_list.append(illegal_info)
                if level > 65:
                    sp['Level'] = {'id': None, 'type': 'IntProperty', 'value': 65}
                    try:
                        exp = PAL_EXP_TABLE['65']['PalTotalEXP']
                    except:
                        exp = 0
                    sp['Exp'] = {'id': None, 'type': 'Int64Property', 'value': exp}
                    changed = True
                if talent_hp > 100:
                    sp['Talent_HP'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 100}}
                    changed = True
                if talent_shot > 100:
                    sp['Talent_Shot'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 100}}
                    changed = True
                if talent_defense > 100:
                    sp['Talent_Defense'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 100}}
                    changed = True
                if rank_hp > 20:
                    sp['Rank_HP'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if rank_attack > 20:
                    sp['Rank_Attack'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if rank_defense > 20:
                    sp['Rank_Defence'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if rank_craftspeed > 20:
                    sp['Rank_CraftSpeed'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if changed:
                    illegals_in_file += 1
        if changed:
            gvasfile_to_sav(gvas_file, file_path)
            result['changed'] = True
            result['actual_pals'] = actual_pals
            result['illegals_fixed'] = illegals_in_file
            result['illegal_entries'] = illegal_entries_list
        else:
            result['actual_pals'] = actual_pals
    except Exception as e:
        print(f'Error processing {filename}: {e}')
    return result
def fix_illegal_pals_in_save(parent=None):
    if not constants.current_save_path or not constants.loaded_level_json:
        return 0
    base_path = constants.get_base_path()
    illegal_log_folder = os.path.join(base_path, 'Illegal Pal Logger')
    if os.path.exists(illegal_log_folder):
        try:
            shutil.rmtree(illegal_log_folder)
        except:
            pass
    players_dir = os.path.join(constants.current_save_path, 'Players')
    total_fixed = 0
    try:
        base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        exp_table_path = os.path.join(base_dir, 'resources', 'game_data', 'pal_exp_table.json')
        PAL_EXP_TABLE = {}
        try:
            with open(exp_table_path, 'r', encoding='utf-8') as f:
                PAL_EXP_TABLE = json.load(f)
        except:
            PAL_EXP_TABLE = {}
        def load_map(fname, key):
            try:
                fp = os.path.join(base_dir, 'resources', 'game_data', fname)
                with open(fp, 'r', encoding='utf-8') as f:
                    js = json.load(f)
                    return {x['asset'].lower(): x['name'] for x in js.get(key, [])}
            except:
                return {}
        PALMAP = load_map('paldata.json', 'pals')
        NPCMAP = load_map('npcdata.json', 'npcs')
        NAMEMAP = {**PALMAP, **NPCMAP}
        owner_nicknames = {}
        player_containers = {}
        players_dir = os.path.join(constants.current_save_path, 'Players')
        if os.path.exists(players_dir):
            player_files = [f for f in os.listdir(players_dir) if f.endswith('.sav') and '_dps' not in f]
            if player_files:
                def load_player_file(filename):
                    try:
                        from palworld_aio.utils import sav_to_gvasfile
                        file_path = os.path.join(players_dir, filename)
                        p_gvas = sav_to_gvasfile(file_path)
                        p_prop = p_gvas.properties.get('SaveData', {}).get('value', {})
                        p_uid_raw = filename.replace('.sav', '')
                        p_uid = p_uid_raw.lower()
                        p_box = p_prop.get('PalStorageContainerId', {}).get('value', {}).get('ID', {}).get('value')
                        p_party = p_prop.get('OtomoCharacterContainerId', {}).get('value', {}).get('ID', {}).get('value')
                        if p_box and p_party:
                            return (p_uid, {'Party': str(p_party).lower(), 'PalBox': str(p_box).lower()})
                    except:
                        pass
                    return None
                from concurrent.futures import ThreadPoolExecutor
                with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
                    results = executor.map(load_player_file, player_files)
                    for result in results:
                        if result:
                            player_containers[result[0]] = result[1]
        cmap = constants.loaded_level_json['properties']['worldSaveData']['value'].get('CharacterSaveParameterMap', {}).get('value', [])
        for item in cmap:
            try:
                raw_p = item.get('value', {}).get('RawData', {}).get('value', {}).get('object', {}).get('SaveParameter', {}).get('value', {})
                if 'IsPlayer' in raw_p:
                    uid = item.get('key', {}).get('PlayerUId', {}).get('value')
                    nn = raw_p.get('NickName', {}).get('value', 'Unknown')
                    if uid:
                        owner_nicknames[str(uid).replace('-', '').lower()] = nn
            except:
                pass
        illegal_pals_by_owner = defaultdict(list)
        illegal_pals_by_player_file = defaultdict(list)
        wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
        cmap = wsd.get('CharacterSaveParameterMap', {}).get('value', [])
        for entry in cmap:
            is_illegal, illegal_markers = check_is_illegal_pal(entry)
            if is_illegal:
                rawf = entry.get('value', {}).get('RawData', {}).get('value', {})
                sp = rawf.get('object', {}).get('SaveParameter', {}).get('value', {})
                level = extract_value(sp, 'Level', 1)
                talent_hp = extract_value(sp, 'Talent_HP', 0)
                talent_shot = extract_value(sp, 'Talent_Shot', 0)
                talent_defense = extract_value(sp, 'Talent_Defense', 0)
                rank_hp = extract_value(sp, 'Rank_HP', 0)
                rank_attack = extract_value(sp, 'Rank_Attack', 0)
                rank_defense = extract_value(sp, 'Rank_Defence', 0)
                rank_craftspeed = extract_value(sp, 'Rank_CraftSpeed', 0)
                cid = extract_value(sp, 'CharacterID', '')
                nick = extract_value(sp, 'NickName', '')
                pal_name = NAMEMAP.get(cid.lower(), cid)
                inst_id = entry.get('key', {}).get('InstanceId', {}).get('value', 'Unknown')
                slot_id_obj = sp.get('SlotId', {})
                if isinstance(slot_id_obj, dict):
                    slot_id_val = slot_id_obj.get('value', slot_id_obj)
                    if isinstance(slot_id_val, dict):
                        container_id_obj = slot_id_val.get('ContainerId', {})
                        if isinstance(container_id_obj, dict):
                            container_id_val = container_id_obj.get('value', container_id_obj)
                            if isinstance(container_id_val, dict):
                                container_id = container_id_val.get('ID', {}).get('value', 'Unknown')
                            else:
                                container_id = str(container_id_val) if container_id_val else 'Unknown'
                        else:
                            container_id = str(container_id_obj) if container_id_obj else 'Unknown'
                    else:
                        container_id = str(slot_id_val) if slot_id_val else 'Unknown'
                else:
                    container_id = str(slot_id_obj) if slot_id_obj else 'Unknown'
                owner_uid = extract_value(sp, 'OwnerPlayerUId', '')
                uid_str = str(owner_uid).replace('-', '').lower() if owner_uid else '00000000000000000000000000000000'
                is_worker = uid_str == '00000000000000000000000000000000'
                guild_id = str(rawf.get('group_id', 'Unknown')).lower()
                base_id = str(container_id).lower() if container_id != 'Unknown' else 'unknown'
                location = 'PalBox Storage'
                if is_worker:
                    location = 'Base Worker'
                    uid_str = f'WORKER_{guild_id}_{base_id}'
                elif owner_uid and str(owner_uid).replace('-', '').lower() in player_containers:
                    containers = player_containers[str(owner_uid).replace('-', '').lower()]
                    if str(container_id).lower() == containers['Party']:
                        location = 'Current Party'
                    elif str(container_id).lower() == containers['PalBox']:
                        location = 'PalBox Storage'
                illegal_info = {'name': pal_name, 'nickname': nick, 'cid': cid, 'level': level, 'talent_hp': talent_hp, 'talent_shot': talent_shot, 'talent_defense': talent_defense, 'rank_hp': rank_hp, 'rank_attack': rank_attack, 'rank_defense': rank_defense, 'rank_craftspeed': rank_craftspeed, 'illegal_markers': illegal_markers, 'instance_id': inst_id, 'container_id': container_id, 'owner_uid': owner_uid, 'location': location}
                illegal_pals_by_owner[uid_str].append(illegal_info)
                changed = False
                if level > 65:
                    sp['Level'] = {'id': None, 'type': 'IntProperty', 'value': 65}
                    try:
                        exp = PAL_EXP_TABLE['65']['PalTotalEXP']
                    except:
                        exp = 0
                    sp['Exp'] = {'id': None, 'type': 'Int64Property', 'value': exp}
                    changed = True
                if talent_hp > 100:
                    sp['Talent_HP'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 100}}
                    changed = True
                if talent_shot > 100:
                    sp['Talent_Shot'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 100}}
                    changed = True
                if talent_defense > 100:
                    sp['Talent_Defense'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 100}}
                    changed = True
                if rank_hp > 20:
                    sp['Rank_HP'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if rank_attack > 20:
                    sp['Rank_Attack'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if rank_defense > 20:
                    sp['Rank_Defence'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if rank_craftspeed > 20:
                    sp['Rank_CraftSpeed'] = {'id': None, 'type': 'ByteProperty', 'value': {'type': 'None', 'value': 20}}
                    changed = True
                if changed:
                    total_fixed += 1
        if os.path.exists(players_dir):
            valid_player_uids = set()
            wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
            group_data_list = wsd.get('GroupSaveDataMap', {}).get('value', [])
            for group in group_data_list:
                if group['value']['GroupType']['value']['value'] == 'EPalGroupType::Guild':
                    raw = group['value']['RawData']['value']
                    players = raw.get('players', [])
                    for p in players:
                        uid_obj = p.get('player_uid')
                        if uid_obj:
                            valid_player_uids.add(str(uid_obj).replace('-', '').lower())
            dps_files = [f for f in os.listdir(players_dir) if f.endswith('.sav') and '_dps' in f and (f.replace('_dps.sav', '').lower() in valid_player_uids)]
            if dps_files:
                print(f'Processing {len(dps_files)} DPS files using {os.cpu_count()} CPU cores...')
                args_list = [(f, players_dir, PAL_EXP_TABLE, NAMEMAP) for f in dps_files]
                with ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
                    futures = {executor.submit(_process_dps_file_worker, args): args[0] for args in args_list}
                    for future in as_completed(futures):
                        filename = futures[future]
                        try:
                            result = future.result()
                            if result['illegals_fixed'] > 0:
                                print(f"Found {result['actual_pals']} pals, fixed {result['illegals_fixed']} illegal pals in {filename}")
                                total_fixed += result['illegals_fixed']
                                for illegal_info in result['illegal_entries']:
                                    illegal_pals_by_player_file[filename].append(illegal_info)
                                    uid_str = str(illegal_info['owner_uid']).replace('-', '').lower() if illegal_info['owner_uid'] else illegal_info['player_uid_from_file'].lower()
                                    illegal_pals_by_owner[uid_str].append(illegal_info)
                        except Exception as e:
                            print(f'Error collecting results from {filename}: {e}')
        base_path = constants.get_base_path()
        illegal_log_dir = os.path.join(base_path, 'Illegal Pal Logger')
        os.makedirs(illegal_log_dir, exist_ok=True)
        guild_illegals = defaultdict(list)
        player_illegals = defaultdict(list)
        for uid, illegals in illegal_pals_by_owner.items():
            if not illegals:
                continue
            if uid.startswith('WORKER_'):
                parts = uid.split('_')
                if len(parts) >= 3:
                    guild_id = parts[1]
                    base_id = parts[2]
                    guild_illegals[guild_id].append((uid, illegals))
            else:
                player_illegals[uid].append((uid, illegals))
        if guild_illegals:
            guilds_illegal_dir = os.path.join(illegal_log_dir, 'Guilds')
            os.makedirs(guilds_illegal_dir, exist_ok=True)
            guild_name_map = {}
            if constants.srcGuildMapping and constants.srcGuildMapping.GroupSaveDataMap:
                for gid_uuid, gdata in constants.srcGuildMapping.GroupSaveDataMap.items():
                    gid = str(gid_uuid)
                    guild_name = gdata['value']['RawData']['value'].get('guild_name', 'Unnamed Guild')
                    guild_name_map[gid.lower()] = guild_name
            else:
                wsd = constants.loaded_level_json['properties']['worldSaveData']['value']
                group_data_list = wsd.get('GroupSaveDataMap', {}).get('value', [])
                for group in group_data_list:
                    if group['value']['GroupType']['value']['value'] == 'EPalGroupType::Guild':
                        raw = group['value']['RawData']['value']
                        guild_id = str(group.get('key', {}).get('GroupID', {}).get('value', ''))
                        guild_name = raw.get('guild_name', 'Unnamed Guild')
                        if guild_id:
                            guild_name_map[guild_id.lower()] = guild_name
            for guild_id, base_illegals_list in guild_illegals.items():
                guild_name = guild_name_map.get(guild_id.lower(), 'Unknown Guild')
                guild_sname = sanitize_filename(guild_name.encode('utf-8', 'replace').decode('utf-8'))
                base_count = len(base_illegals_list)
                total_illegals = sum((len(illegals) for _, illegals in base_illegals_list))
                guild_dir = os.path.join(guilds_illegal_dir, f'({guild_id})_({guild_sname})_({base_count})')
                os.makedirs(guild_dir, exist_ok=True)
                for uid, illegals in base_illegals_list:
                    if not illegals:
                        continue
                    parts = uid.split('_')
                    base_id = parts[2] if len(parts) >= 3 else uid
                    pname = owner_nicknames.get(uid, f'Base_{base_id}')
                    sname = sanitize_filename(pname.encode('utf-8', 'replace').decode('utf-8'))
                    pal_count = len(illegals)
                    log_file = os.path.join(guild_dir, f'({base_id})_({pal_count}_illegals).log')
                    logger_name = ''.join((c if c.isalnum() or c in ('_', '-') else '_' for c in f'illegal_lg_{uid}'))
                    logger = logging.getLogger(logger_name)
                    logger.setLevel(logging.INFO)
                    logger.propagate = False
                    for h in logger.handlers[:]:
                        h.flush()
                        h.close()
                        logger.removeHandler(h)
                    try:
                        handler = logging.FileHandler(log_file, mode='w', encoding='utf-8', errors='replace')
                        handler.setFormatter(logging.Formatter('%(message)s'))
                        logger.addHandler(handler)
                    except:
                        continue
                    logger.info('=' * 80)
                    logger.info(f'ILLEGAL PALS LOG: {pname}')
                    logger.info(f'Total Illegal Pals Found: {pal_count}')
                    logger.info('=' * 80)
                    logger.info('')
                    by_location = defaultdict(list)
                    for info in illegals:
                        by_location[info['location']].append(info)
                    prio = ['DPS Storage', 'Current Party', 'PalBox Storage', 'Base Worker']
                    sorted_locations = prio + sorted([k for k in by_location.keys() if k not in prio])
                    for location in sorted_locations:
                        if location not in by_location:
                            continue
                        pals = by_location[location]
                        logger.info(f'\n{location} (Count: {len(pals)})')
                        logger.info('-' * 40)
                        for info in pals:
                            display_name = info['name']
                            if info.get('nickname') and info['nickname'] not in ('Unknown', ''):
                                display_name = f"{info['name']}(Nickname: {info['nickname']})"
                            illegal_str = ', '.join(info['illegal_markers'])
                            lvl_str = f"[!] {info['level']}" if 'Level' in info['illegal_markers'] else str(info['level'])
                            iv_str = f"HP: {info['talent_hp']}(+0%),ATK: {info['talent_shot']}(+0%),DEF: {info['talent_defense']}(+0%)"
                            soul_str = f"HP Soul: {info['rank_hp']}, ATK Soul: {info['rank_attack']}, DEF Soul: {info['rank_defense']}, Craft: {info['rank_craftspeed']}"
                            info_block = f'\n[{display_name}]\n'
                            info_block += f'  [!] ILLEGAL: {illegal_str}\n'
                            info_block += f'  Level: {lvl_str}\n'
                            info_block += f'  IVs:      {iv_str}\n'
                            info_block += f'  Souls:    {soul_str}\n'
                            info_block += f"  IDs:      Container: {info['container_id']} | Instance: {info['instance_id']}\n"
                            logger.info(info_block)
                            logger.info('-' * 20)
                    for h in logger.handlers[:]:
                        h.flush()
                        h.close()
                        logger.removeHandler(h)
        if player_illegals:
            players_illegal_dir = os.path.join(illegal_log_dir, 'Players')
            os.makedirs(players_illegal_dir, exist_ok=True)
            for uid, illegals_list in player_illegals.items():
                for _, illegals in illegals_list:
                    if not illegals:
                        continue
                    pname = owner_nicknames.get(uid, f'Player_{uid[:8]}')
                    sname = sanitize_filename(pname.encode('utf-8', 'replace').decode('utf-8'))
                    pal_count = len(illegals)
                    log_file = os.path.join(players_illegal_dir, f'({uid})_({sname})_({pal_count}_illegals).log')
                    logger_name = ''.join((c if c.isalnum() or c in ('_', '-') else '_' for c in f'illegal_lg_{uid}'))
                    logger = logging.getLogger(logger_name)
                    logger.setLevel(logging.INFO)
                    logger.propagate = False
                    for h in logger.handlers[:]:
                        h.flush()
                        h.close()
                        logger.removeHandler(h)
                    try:
                        handler = logging.FileHandler(log_file, mode='w', encoding='utf-8', errors='replace')
                        handler.setFormatter(logging.Formatter('%(message)s'))
                        logger.addHandler(handler)
                    except:
                        continue
                    logger.info('=' * 80)
                    logger.info(f'ILLEGAL PALS LOG: {pname}')
                    logger.info(f'Total Illegal Pals Found: {pal_count}')
                    logger.info('=' * 80)
                    logger.info('')
                    by_location = defaultdict(list)
                    for info in illegals:
                        by_location[info['location']].append(info)
                    prio = ['DPS Storage', 'Current Party', 'PalBox Storage', 'Base Worker']
                    sorted_locations = prio + sorted([k for k in by_location.keys() if k not in prio])
                    for location in sorted_locations:
                        if location not in by_location:
                            continue
                        pals = by_location[location]
                        logger.info(f'\n{location} (Count: {len(pals)})')
                        logger.info('-' * 40)
                        for info in pals:
                            display_name = info['name']
                            if info.get('nickname') and info['nickname'] not in ('Unknown', ''):
                                display_name = f"{info['name']}(Nickname: {info['nickname']})"
                            illegal_str = ', '.join(info['illegal_markers'])
                            lvl_str = f"[!] {info['level']}" if 'Level' in info['illegal_markers'] else str(info['level'])
                            iv_str = f"HP: {info['talent_hp']}(+0%),ATK: {info['talent_shot']}(+0%),DEF: {info['talent_defense']}(+0%)"
                            soul_str = f"HP Soul: {info['rank_hp']}, ATK Soul: {info['rank_attack']}, DEF Soul: {info['rank_defense']}, Craft: {info['rank_craftspeed']}"
                            info_block = f'\n[{display_name}]\n'
                            info_block += f'  [!] ILLEGAL: {illegal_str}\n'
                            info_block += f'  Level: {lvl_str}\n'
                            info_block += f'  IVs:      {iv_str}\n'
                            info_block += f'  Souls:    {soul_str}\n'
                            info_block += f"  IDs:      Container: {info['container_id']} | Instance: {info['instance_id']}\n"
                            logger.info(info_block)
                            logger.info('-' * 20)
                    for h in logger.handlers[:]:
                        h.flush()
                        h.close()
                        logger.removeHandler(h)
        print(f'Created illegal pal logs in: {illegal_log_dir}')
    except Exception as e:
        import traceback
        traceback.print_exc()
        return 0
    return total_fixed