Skip to content

Instantly share code, notes, and snippets.

@AmauryCarrade
Last active May 13, 2023 19:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AmauryCarrade/14f262ba5ff286c47f0c724445dc1ae7 to your computer and use it in GitHub Desktop.
Save AmauryCarrade/14f262ba5ff286c47f0c724445dc1ae7 to your computer and use it in GitHub Desktop.
Minecraft Server Stats Leaderboard
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()
[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