import json
import struct
from collections import Counter
"""
Use https://robojumper.github.io/DarkestDungeonSaveEditor/ to convert a profile's ".json" files to actual JSON.
Then paste in the file paths to your actual JSON files below.
"""
roster_file = r"persist.roster.json"
log_file = r"persist.campaign_log.json"
def stringHash(s):
"""
Python implementation of Darkest Dungeon's string hasher as described in https://github.com/robojumper/DarkestDungeonSaveEditor/blob/master/docs/dson.md
"""
hash_val = 0
# Encode the string to bytes for iteration
byte_array = s.encode('utf-8')
for byte in byte_array:
# Multiply hash by 53 and add the unsigned byte value
hash_val = hash_val * 53 + byte
# Simulate 32-bit integer overflow
hash_val &= 0xFFFFFFFF
# Convert the unsigned 32-bit hash to a signed 32-bit integer
return struct.unpack('i', struct.pack('I', hash_val))[0]
def create_hash_to_class_map(roster_file_path):
"""
Parses persist.roster.json to create a mapping from class hash to class name.
Args:
roster_file_path (str): The path to the roster JSON file.
Returns:
dict: A dictionary mapping integer hash values to string class names.
Returns an empty dictionary if the file cannot be read or parsed.
"""
hash_map = {}
try:
with open(roster_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: The file '{roster_file_path}' was not found.")
return {}
except json.JSONDecodeError:
print(f"Error: Could not decode JSON from '{roster_file_path}'.")
return {}
heroes_data = data.get('base_root', {}).get('heroes', {})
for hero_id, hero_data in heroes_data.items():
hero_class = hero_data.get('hero_file_data', {}).get('raw_data', {}).get('base_root', {}).get('heroClass')
if hero_class and isinstance(hero_class, str):
cleaned_class = hero_class.replace('###', '')
hash_val = stringHash(cleaned_class)
hash_map[hash_val] = cleaned_class
return hash_map
def parse_quest_log_and_count_classes(log_file_path, hash_map):
"""
Parses persist.campaign_log.json to count hero class occurrences in successful quests,
resolving hashed IDs to class names using the provided hash map.
If a hash cannot be resolved, the raw hash is used as the class ID.
Args:
log_file_path (str): The path to the JSON log file.
hash_map (dict): A dictionary mapping integer class hashes to string names.
Returns:
collections.Counter: A Counter object with class names (or unresolved hashes)
as keys and their counts as values.
"""
try:
with open(log_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: The file '{log_file_path}' was not found.")
return Counter()
except json.JSONDecodeError:
print(f"Error: The file '{log_file_path}' is not a valid JSON file.")
return Counter()
class_counts = Counter()
chapters = data.get('base_root', {}).get('chapters', {})
for chapter_content in chapters.values():
if not isinstance(chapter_content, dict):
continue
for entry_content in chapter_content.values():
# Identify quest-success entries
if (isinstance(entry_content, dict) and
entry_content.get('rtti') == 2006063882 and
entry_content.get('success') is True):
heroes = entry_content.get('heroes', {})
for hero_details in heroes.values():
hero_class_id = hero_details.get('class')
if hero_class_id is not None:
class_name = None
if isinstance(hero_class_id, str):
# Handle string IDs like "###crusader"
class_name = hero_class_id.replace('###', '')
elif isinstance(hero_class_id, int):
# Resolve integer hashes using the map. If not found, use the hash as a string.
class_name = hash_map.get(hero_class_id, str(hero_class_id))
if class_name:
class_counts[class_name] += 1
return class_counts
# --- Execution ---
if __name__ == "__main__":
# Step 1: Create the hash map from the roster file
hero_hash_map = create_hash_to_class_map(roster_file)
if hero_hash_map:
# Step 2: Parse the log file and count classes using the hash map
hero_class_counts = parse_quest_log_and_count_classes(log_file, hero_hash_map)
if hero_class_counts:
print("Class counts in successful quests:")
# Sort items by count in descending order for better readability
for class_id, count in hero_class_counts.most_common():
print(f"- {class_id}: {count}")
else:
print("No successful quest data found in campaign_log.")
else:
print("Could not create the class hash map.")