diff --git a/prikolv3_1.py b/prikolv3_1.py new file mode 100644 index 0000000..9c2d68d --- /dev/null +++ b/prikolv3_1.py @@ -0,0 +1,496 @@ +import os +import json +import requests +import argparse +from collections import defaultdict +from tqdm import tqdm # For progress bars +import time # For delays between API requests +import csv # For writing CSV + +# Cache file name +CACHE_FILE = "uuid_cache.json" + +# Delay in seconds between API requests +API_DELAY = 1.0 # 1 second delay to avoid rate limiting + +# Function to load cache +def load_cache(world_path): + cache_path = os.path.join(world_path, CACHE_FILE) + if os.path.exists(cache_path): + print(f"Loading UUID cache from {cache_path}") + with open(cache_path, 'r') as f: + cache = json.load(f) + print(f"Loaded {len(cache)} cached usernames") + return cache + else: + print("No UUID cache found, starting fresh") + return {} + +# Function to save cache +def save_cache(world_path, cache): + cache_path = os.path.join(world_path, CACHE_FILE) + print(f"Saving UUID cache to {cache_path}") + with open(cache_path, 'w') as f: + json.dump(cache, f, indent=4) + print("Cache saved") + +# Function to convert UUID to Minecraft username using Mojang API or cache +def uuid_to_username(uuid, cache, world_path): + if uuid in cache: + print(f"Using cached username for UUID {uuid}: {cache[uuid]}") + return cache[uuid] + + print(f"Converting uncached UUID {uuid} to username...") + time.sleep(API_DELAY) # Delay before making API request + try: + url = f"https://api.mojang.com/user/profile/{uuid.replace('-', '')}" + print(f"Requesting Mojang API: {url}") + response = requests.get(url) + if response.status_code == 200: + username = response.json()['name'] + print(f"Successfully got username: {username}") + cache[uuid] = username + save_cache(world_path, cache) # Save immediately after adding + return username + else: + fallback = f"Unknown_{uuid[:8]}" + print(f"API request failed (status {response.status_code}), using fallback: {fallback}") + cache[uuid] = fallback + save_cache(world_path, cache) # Save immediately after adding + return fallback + except Exception as e: + fallback = f"Unknown_{uuid[:8]}" + print(f"Exception during API request: {e}, using fallback: {fallback}") + cache[uuid] = fallback + save_cache(world_path, cache) # Save immediately after adding + return fallback + +def get_stat_value(stats, path): + """Get stat value by path like 'mined' for total, 'mined:stone', 'custom:play_time'""" + print(f"Getting stat value for path '{path}'...") + if ':' not in path: + cat = f"minecraft:{path}" + if cat in stats and isinstance(stats[cat], dict): + total = sum(stats[cat].values()) + print(f"Total for category '{path}': {total}") + return total + print(f"No data for category '{path}'") + return 0 + else: + cat, item = path.split(':', 1) + cat_key = f"minecraft:{cat}" + item_key = f"minecraft:{item}" + if cat_key in stats and isinstance(stats[cat_key], dict) and item_key in stats[cat_key]: + value = stats[cat_key][item_key] + print(f"Value for '{path}': {value}") + return value + print(f"No data for '{path}'") + return 0 + +def get_value(stats, info): + """Get formatted value for predefined categories""" + print(f"Getting formatted value for category...") + if 'extractor' in info: + value = info['extractor'](stats) + print(f"Extractor value: {value}") + return value + path = info['path'] + raw = get_stat_value(stats, path) + div = info.get('div', 1) + formatted = raw / div + print(f"Formatted value ({raw} / {div}): {formatted}") + return formatted + +def get_categories(player_stats): + print("Collecting available categories from stats...") + cats = set() + total_players = len(player_stats) + progress_bar = tqdm(total=total_players, desc="Processing categories", unit="player") + for uuid, stats in player_stats.items(): + print(f"Processing categories for UUID {uuid}") + for cat_full in stats: + if cat_full.startswith('minecraft:'): + cat = cat_full[10:] # remove 'minecraft:' + cats.add(cat) + progress_bar.update(1) + progress_bar.close() + sorted_cats = sorted(cats) + print(f"Found categories: {sorted_cats}") + return sorted_cats + +def get_items(cat, player_stats): + print(f"Collecting unique items in category '{cat}'...") + items = set() + total_players = len(player_stats) + progress_bar = tqdm(total=total_players, desc=f"Processing items in '{cat}'", unit="player") + for uuid, stats in player_stats.items(): + print(f"Processing items for UUID {uuid} in '{cat}'") + cat_key = f'minecraft:{cat}' + if cat_key in stats and isinstance(stats[cat_key], dict): + for item_full in stats[cat_key]: + item = item_full[10:] # remove 'minecraft:' + items.add(item) + progress_bar.update(1) + progress_bar.close() + sorted_items = sorted(items) + print(f"Found {len(sorted_items)} unique items in '{cat}'") + return sorted_items + +# Formatters for categories: (divisor, unit) +formatters = { + 'custom:play_time': (72000, 'hours'), # 20 ticks/sec * 3600 sec/hour + 'custom:total_world_time': (72000, 'hours'), + 'custom:damage_dealt': (10, 'hearts'), + 'custom:damage_taken': (10, 'hearts'), + 'custom:walk_one_cm': (100000, 'km'), + 'custom:sprint_one_cm': (100000, 'km'), + 'custom:fly_one_cm': (100000, 'km'), + 'mined': (1, 'blocks'), + 'crafted': (1, 'items'), + 'used': (1, 'uses'), + 'killed': (1, 'kills'), + 'killed_by': (1, 'deaths'), + 'custom:mob_kills': (1, 'kills'), + 'custom:player_kills': (1, 'kills'), + 'custom:deaths': (1, ''), + 'picked_up': (1, 'items'), + 'dropped': (1, 'items'), + 'broken': (1, 'items'), + # Add more if needed +} + +# Predefined categories (for default mode) +categories = { + 'play_time': { + 'path': 'custom:play_time', + 'div': 72000, + 'desc': 'Top players by play time (hours)' + }, + 'blocks_mined': { + 'path': 'mined', + 'div': 1, + 'desc': 'Top players by blocks mined' + }, + 'mobs_killed': { + 'path': 'custom:mob_kills', + 'div': 1, + 'desc': 'Top players by mobs killed' + }, + 'damage_dealt': { + 'path': 'custom:damage_dealt', + 'div': 10, + 'desc': 'Top players by damage dealt (hearts)' + }, + 'distance_traveled': { + 'extractor': lambda s: sum([ + s.get('minecraft:custom', {}).get('minecraft:walk_one_cm', 0), + s.get('minecraft:custom', {}).get('minecraft:sprint_one_cm', 0), + s.get('minecraft:custom', {}).get('minecraft:fly_one_cm', 0) + ]) / 100000, + 'desc': 'Top players by distance traveled (km)' + } +} + +def write_top_to_file(world_path, path, sorted_players, div, unit, precision, cache): + filename = f"top_{path.replace(':', '_')}.txt" + output_path = os.path.join(world_path, filename) + print(f"Writing top for '{path}' to file: {output_path}") + with open(output_path, 'w', encoding='utf-8') as f: + f.write(f"Top players by '{path}':\n") + for i, (uuid, value) in enumerate(sorted_players, 1): + username = uuid_to_username(uuid, cache, world_path) # Pass world_path + fvalue = value / div + line = f"{i}. {username}: {fvalue:.{precision}f}" + if unit: + line += f" {unit}" + f.write(line + "\n") + print(f"Written {len(sorted_players)} players to {output_path}") + +def main(world_path, args): + stats_dir = os.path.join(world_path, 'stats') + print(f"Checking stats directory: {stats_dir}") + + if not os.path.exists(stats_dir): + print(f"Stats directory not found: {stats_dir}") + return + + # Load UUID cache + uuid_cache = load_cache(world_path) + + player_stats = {} + print("Starting to read player stats files...") + + # Get list of JSON files + json_files = [f for f in os.listdir(stats_dir) if f.endswith('.json')] + total_files = len(json_files) + print(f"Found {total_files} player stats files") + progress_bar = tqdm(total=total_files, desc="Loading player stats", unit="file") + + # Read all JSON files in stats directory, store by UUID + for filename in json_files: + uuid = filename[:-5] + filepath = os.path.join(stats_dir, filename) + print(f"Processing file: {filepath} (UUID: {uuid})") + + try: + with open(filepath, 'r') as f: + data = json.load(f) + raw_stats = data.get('stats', {}) + print(f"Successfully loaded stats for UUID {uuid}") + player_stats[uuid] = raw_stats + print(f"Added UUID {uuid} to stats") + except json.JSONDecodeError as e: + print(f"Invalid JSON in {filepath}: {e}") + progress_bar.update(1) + progress_bar.close() + + if not player_stats: + print("No player stats found.") + return + + print(f"Loaded stats for {len(player_stats)} players") + + if args.list_categories: + print("Listing available categories...") + cats = get_categories(player_stats) + content = "Available categories (use --stat 'cat' for total or --stat 'cat:item' for specific):\n" + for c in cats: + content += f" {c}\n" + content += "\nExamples:\n" + content += " --stat mined # total blocks mined\n" + content += " --stat mined:stone # stone mined\n" + content += " --stat killed:creeper # creepers killed\n" + content += " --stat custom:play_time # play time\n" + print(content) + output_path = os.path.join(world_path, "categories.txt") + with open(output_path, 'w') as f: + f.write(content) + print(f"Written categories to {output_path}") + save_cache(world_path, uuid_cache) + return + + if args.list_items: + cat = args.list_items + print(f"Listing items in category '{cat}'...") + items_list = get_items(cat, player_stats) + content = f"Unique items in '{cat}' (sorted alphabetically):\n" + for item in items_list: + content += f" {item}\n" + print(content) + output_path = os.path.join(world_path, f"items_{cat}.txt") + with open(output_path, 'w') as f: + f.write(content) + print(f"Written items to {output_path}") + save_cache(world_path, uuid_cache) + return + + top_n = args.top if args.top > 0 else len(player_stats) # If --top=0, all players + + if args.all_categories: + print("Generating tops for all categories and items...") + cats = get_categories(player_stats) + + # Calculate total tasks + total_tasks = 0 + for category in cats: + items = get_items(category, player_stats) # Pre-fetch items to count + total_tasks += len(items) + if category != 'custom': + total_tasks += 1 # For total + print(f"Total tasks to process: {total_tasks}") + + global_progress = tqdm(total=total_tasks, desc="Overall progress", unit="task", position=1, leave=True) + + all_tops = [] # Collect all data + + for category in cats: + print(f"Processing category '{category}'...") + + # Generate total for category + if category != 'custom': + print(f"Generating total top for '{category}'...") + player_values = {} + total_players = len(player_stats) + progress_bar = tqdm(total=total_players, desc=f"Calculating total '{category}' values", unit="player", position=0, leave=False) + for uuid, stats in player_stats.items(): + player_values[uuid] = get_stat_value(stats, category) + progress_bar.update(1) + progress_bar.close() + + sorted_players = sorted( + player_values.items(), + key=lambda item: item[1], + reverse=True + ) + print(f"Sorted {len(sorted_players)} players for total '{category}'") + + div, unit = formatters.get(category, (1, '')) + precision = 2 if div > 1 else 0 + + for i, (uuid, value) in enumerate(sorted_players[:top_n], 1): + username = uuid_to_username(uuid, uuid_cache, world_path) + fvalue = round(value / div, precision) + all_tops.append({ + 'stat': category, + 'rank': i, + 'username': username, + 'uuid': uuid, + 'raw_value': value, + 'formatted_value': fvalue, + 'unit': unit + }) + global_progress.update(1) + + # Get unique items for the category + items = get_items(category, player_stats) + + # Generate top for each item + for item in items: + path = f"{category}:{item}" + print(f"Generating top for '{path}'...") + + player_values = {} + total_players = len(player_stats) + progress_bar = tqdm(total=total_players, desc=f"Calculating '{path}' values", unit="player", position=0, leave=False) + for uuid, stats in player_stats.items(): + player_values[uuid] = get_stat_value(stats, path) + progress_bar.update(1) + progress_bar.close() + + sorted_players = sorted( + player_values.items(), + key=lambda item: item[1], + reverse=True + ) + print(f"Sorted {len(sorted_players)} players for '{path}'") + + div, unit = formatters.get(category, (1, '')) # Use category formatter for items + precision = 2 if div > 1 else 0 + + for i, (uuid, value) in enumerate(sorted_players[:top_n], 1): + username = uuid_to_username(uuid, uuid_cache, world_path) + fvalue = round(value / div, precision) + all_tops.append({ + 'stat': path, + 'rank': i, + 'username': username, + 'uuid': uuid, + 'raw_value': value, + 'formatted_value': fvalue, + 'unit': unit + }) + global_progress.update(1) + + global_progress.close() + + # Write all to single CSV + output_path = os.path.join(world_path, "all_tops.csv") + print(f"Writing all tops to CSV: {output_path}") + with open(output_path, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['stat', 'rank', 'username', 'uuid', 'raw_value', 'formatted_value', 'unit'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + for row in all_tops: + writer.writerow(row) + print(f"Written {len(all_tops)} rows to {output_path}") + + save_cache(world_path, uuid_cache) + return + + if args.stat: + path = args.stat + print(f"Generating top for stat '{path}'...") + + player_values = {} + total_players = len(player_stats) + progress_bar = tqdm(total=total_players, desc="Calculating stat values", unit="player") + for uuid, stats in player_stats.items(): + player_values[uuid] = get_stat_value(stats, path) + progress_bar.update(1) + progress_bar.close() + + sorted_players = sorted( + player_values.items(), + key=lambda item: item[1], + reverse=True + ) + print(f"Sorted {len(sorted_players)} players") + div, unit = formatters.get(path, (1, '')) + precision = 2 if div > 1 else 0 + print(f"Using formatter: divisor={div}, unit='{unit}', precision={precision}") + content = f"Top players by '{path}':\n" + + output_progress = tqdm(total=min(top_n, len(sorted_players)), desc="Converting and outputting top players", unit="player") + for i, (uuid, value) in enumerate(sorted_players[:top_n], 1): + username = uuid_to_username(uuid, uuid_cache, world_path) + fvalue = value / div + line = f"{i}. {username}: {fvalue:.{precision}f}" + if unit: + line += f" {unit}" + content += line + "\n" + print(line) + output_progress.update(1) + output_progress.close() + output_path = os.path.join(world_path, f"top_{path.replace(':', '_')}.txt") + with open(output_path, 'w') as f: + f.write(content) + print(f"Written to {output_path}") + save_cache(world_path, uuid_cache) + return + + # Default: predefined tops + print("Generating predefined tops...") + for cat_key, cat_info in categories.items(): + print(f"Processing predefined category '{cat_key}': {cat_info['desc']}") + + player_values = {} + total_players = len(player_stats) + progress_bar = tqdm(total=total_players, desc=f"Calculating '{cat_key}' values", unit="player") + for uuid, stats in player_stats.items(): + player_values[uuid] = get_value(stats, cat_info) + progress_bar.update(1) + progress_bar.close() + + sorted_players = sorted( + player_values.items(), + key=lambda item: item[1], + reverse=True + ) + print(f"Sorted {len(sorted_players)} players for '{cat_key}'") + content = f"{cat_info['desc']}:\n" + + output_progress = tqdm(total=min(top_n, len(sorted_players)), desc="Converting and outputting top players", unit="player") + for i, (uuid, value) in enumerate(sorted_players[:top_n], 1): + username = uuid_to_username(uuid, uuid_cache, world_path) + line = f"{i}. {username}: {value:.2f}" + content += line + "\n" + print(line) + output_progress.update(1) + output_progress.close() + output_path = os.path.join(world_path, f"top_{cat_key}.txt") + with open(output_path, 'w') as f: + f.write(content) + print(f"Written to {output_path}") + + save_cache(world_path, uuid_cache) + +if __name__ == "__main__": + print("Starting script...") + parser = argparse.ArgumentParser(description="Generate Minecraft player tops from world stats") + parser.add_argument("world_path", help="Path to the Minecraft world directory") + parser.add_argument("--stat", help="Stat path e.g. 'mined:stone', 'killed:creeper', 'mined' for total") + parser.add_argument("--list-categories", action="store_true", help="List available categories") + parser.add_argument("--list-items", help="List unique items in a category e.g. 'mined'") + parser.add_argument("--all-categories", action="store_true", help="Generate tops for all categories and items (all players)") + parser.add_argument("--top", type=int, default=0, help="Number of top players to show (0 for all, default all for --all-categories)") + args = parser.parse_args() + print(f"Parsed arguments: {vars(args)}") + + # Mutual exclusivity check + active_modes = [args.stat, args.list_categories, args.list_items, args.all_categories] + if sum(bool(x) for x in active_modes) > 1: + print("Use only one of --stat, --list-categories, --list-items, --all-categories") + exit(1) + + main(args.world_path, args) + print("Script execution completed.") \ No newline at end of file