Last active
May 13, 2023 19:55
-
-
Save AmauryCarrade/14f262ba5ff286c47f0c724445dc1ae7 to your computer and use it in GitHub Desktop.
Minecraft Server Stats Leaderboard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import glob | |
import functools | |
import json | |
import pathlib | |
import uuid | |
import os | |
import click | |
import requests | |
UUID_CACHE_FILENAME = ".uuid-cache.json" | |
UUID_CACHE = {} | |
EXCLUDED_STATS = ["minecraft:custom"] | |
def loads_uuid_cache(): | |
""" | |
Initializes the UUID cache, loading existing entries from the cache file. | |
""" | |
global UUID_CACHE | |
# Loads cache if available | |
try: | |
with open(UUID_CACHE_FILENAME, "r") as f: | |
UUID_CACHE = json.load(f) | |
except: | |
pass | |
def dumps_uuid_cache(): | |
""" | |
Dumps in-memory cache to the cache file for subsequent usages. | |
""" | |
with open(UUID_CACHE_FILENAME, "w") as f: | |
json.dump(UUID_CACHE, f, indent=2) | |
def get_name_from_uuid(uuid: uuid.UUID) -> str: | |
""" | |
Obtains an username from a Mojang UUID. | |
Uses the cache file if possible, else asks Mojang for it. | |
""" | |
uuid_str = str(uuid) | |
if uuid_str in UUID_CACHE: | |
return UUID_CACHE[uuid_str] | |
username = None | |
r: requests.Response = requests.get( | |
url=f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid_str}" | |
) | |
if r.status_code != 204 and r.status_code <= 300: | |
profile: dict = json.loads(r.text) | |
username = profile.get("name") | |
UUID_CACHE[uuid_str] = username | |
return username | |
@click.command() | |
@click.option( | |
"-w", | |
"--world", | |
type=click.Path( | |
exists=True, | |
dir_okay=True, | |
file_okay=False, | |
readable=True, | |
path_type=pathlib.Path, | |
), | |
required=True, | |
help="Path to the root of the Minecraft world to analyze.", | |
) | |
@click.option( | |
"-c", "--count", default=10, help="Amount of players to display in the leaderboard." | |
) | |
def main(world, count=10): | |
loads_uuid_cache() | |
# Structure: | |
# - first-level key is a statistics key | |
# - second-level key is a player uuid | |
# - second-level value is the cumulative statistics for this stat & player | |
leaderboard = {} | |
# Extract all statistics | |
for file in glob.glob(f"{world / 'stats'}/*.json"): | |
player_uuid = uuid.UUID(os.path.basename(file).replace(".json", "")) | |
with open(file, "r") as f: | |
player_stats: dict = json.load(f) | |
for stat_type, stats in player_stats["stats"].items(): | |
if stat_type in EXCLUDED_STATS: | |
continue | |
cumulative = functools.reduce( | |
lambda acc, stat: acc + stat, stats.values(), 0 | |
) | |
leaderboard.setdefault(stat_type, {})[str(player_uuid)] = cumulative | |
# Sort stats | |
for stat_type, lead in leaderboard.items(): | |
leaderboard[stat_type] = dict( | |
sorted(lead.items(), key=lambda item: item[1], reverse=True) | |
) | |
# Display | |
for stat_type, lead in leaderboard.items(): | |
click.secho(f"Leaderboard pour {stat_type}", bold=True, fg="green") | |
for index, (player_uuid, cumulative) in zip(range(count), lead.items()): | |
player_uuid = uuid.UUID(player_uuid) | |
click.echo( | |
f"{index+1:02}. " | |
+ click.style( | |
get_name_from_uuid(player_uuid).ljust(18), bold=True, fg="blue" | |
) | |
+ f" - {format(cumulative, '9,d').replace(',', ' ')}" | |
) | |
click.echo() | |
dumps_uuid_cache() | |
if __name__ == "__main__": | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[tool.poetry] | |
name = "mc-stats-comparatives" | |
version = "0.1.0" | |
description = "Generates leaderboards from Minecraft server statistics" | |
authors = ["Amaury Carrade <amaury@carrade.eu>"] | |
license = "CECILL-B" | |
readme = "README.md" | |
[tool.poetry.dependencies] | |
python = "^3.10" | |
click = "^8.1.3" | |
requests = "^2.30.0" | |
[build-system] | |
requires = ["poetry-core"] | |
build-backend = "poetry.core.masonry.api" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment