Skip to content

Instantly share code, notes, and snippets.

@sschr15
Created June 10, 2025 05:43
Show Gist options
  • Save sschr15/95b132d708e9d3df92424080e88b9fbf to your computer and use it in GitHub Desktop.
Save sschr15/95b132d708e9d3df92424080e88b9fbf to your computer and use it in GitHub Desktop.
Using Quilt's hashed mappings, count how many unique mappings have ever existed
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from hashlib import sha1
from os import path, makedirs
from requests import get
from sys import stderr
from tempfile import gettempdir
HEADERS = {
'User-Agent': 'sschr15 - unique hash counter',
}
def get_hash_versions() -> list[str]:
"""
Fetches the collection of Minecraft versions with hashed mappings.
This is equivalent to every version since 1.14.4, unless Mojang
or QuiltMC stop producing mappings in the future.
(i.e., releases, snapshots, pre-releases, experimental versions, and April Fools' versions).
:return: A list of (non-normalized) Minecraft versions
"""
hashed_metadata_url = 'https://meta.quiltmc.org/v3/versions/game/hashed'
response = get(hashed_metadata_url, headers=HEADERS)
response.raise_for_status()
return response.json()
def download_hashes(version: str, use_temp: bool = False) -> str:
"""
Downloads the hashes for a specific Minecraft version.
:param version: The Minecraft version to download hashes for.
:param use_temp: If True, the hashes will be downloaded to a temporary file.
If False, the hashes will be downloaded to the `hashes` directory.
:return: The location of the downloaded hashes file.
"""
hashes_url_base = 'https://maven.quiltmc.org/repository/release/org/quiltmc/hashed'
if use_temp:
hashes_dir = path.join(gettempdir(), 'hashes')
else:
hashes_dir = path.join(path.dirname(__file__), 'hashes')
makedirs(hashes_dir, exist_ok=True)
hashes_file = path.join(hashes_dir, f'{version}.tiny')
if not path.exists(hashes_file):
hashes_url = f'{hashes_url_base}/{version}/hashed-{version}.tiny'
response = get(hashes_url, headers=HEADERS)
response.raise_for_status()
mappings_sha1 = sha1(response.content).hexdigest()
checksum_response = get(f'{hashes_url}.sha1', headers=HEADERS)
checksum_response.raise_for_status()
if mappings_sha1 != checksum_response.text.strip():
raise ValueError(f'Checksum mismatch for {version}: expected {mappings_sha1}, got {checksum_response.text.strip()}')
with open(hashes_file, 'w', encoding='utf-8') as file:
file.write(response.text)
return hashes_file
def get_hashes(version_file: str) -> tuple[set[str], set[str], set[str]]:
"""
Reads the hashes file and returns the version, hash, and mappings.
:param version_file: The path to the hashes file.
:return: A tuple containing three sets: one each for classes, methods, and fields.
"""
classes = set()
methods = set()
fields = set()
with open(version_file, 'r', encoding='utf-8') as file:
for line in file:
if line == 'tiny\t2\t0\tofficial\thashed\n':
# Skip the header line
continue
if line.startswith('#'):
continue
parts = line.strip().split('\t')
if len(parts) < 3:
print(f'Invalid line in file: {line.strip()}', file=stderr)
continue
if parts[0] not in 'cmf':
print(f'Unknown type in line: {line.strip()}', file=stderr)
continue
match parts[0]:
case 'c': # classes (or javadoc)
if not line.startswith('c'):
# skip javadoc
continue
_, _, full_class_name = parts
class_name = full_class_name.split('/')[-1]
classes.add(class_name)
case 'm': # methods
_, _, _, method_name = parts
methods.add(method_name)
case 'f':
_, _, _, field_name = parts
fields.add(field_name)
return classes, methods, fields
def count_hashes(version_file: str) -> tuple[int, int, int]:
"""
Counts the number of unique classes, methods, and fields in the hashes file.
:param version_file: The path to the hashes file.
:return: A tuple containing the counts of classes, methods, and fields.
"""
classes, methods, fields = get_hashes(version_file)
return len(classes), len(methods), len(fields)
def main():
import argparse
parser = argparse.ArgumentParser(description='Count unique hashes in Minecraft version files.')
parser.add_argument('--version', type=str, help='Minecraft version to count hashes for. Ignored if --all-versions is set.', default='1.20.4')
parser.add_argument('--all-versions', action='store_true', help='Count hashes for all available Minecraft versions.')
parser.add_argument('--temp', action='store_true', help='Use a temporary directory for hashes.')
args = parser.parse_args()
if args.all_versions:
try:
versions = get_hash_versions()
all_classes = set()
all_methods = set()
all_fields = set()
for version in versions:
print(f'Processing version: {version}')
version_file = download_hashes(version, use_temp=args.temp)
classes, methods, fields = get_hashes(version_file)
all_classes.update(classes)
all_methods.update(methods)
all_fields.update(fields)
print(f'Total unique hashes across all versions:')
print(f'Classes: {len(all_classes)}, Methods: {len(all_methods)}, Fields: {len(all_fields)}')
except Exception as e:
print(f'Error: {e}', file=stderr)
else:
try:
version_file = download_hashes(args.version, use_temp=args.temp)
classes_count, methods_count, fields_count = count_hashes(version_file)
print(f'Classes: {classes_count}, Methods: {methods_count}, Fields: {fields_count}')
except Exception as e:
print(f'Error: {e}', file=stderr)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment