Skip to content

Instantly share code, notes, and snippets.

@bearlikelion
Created February 7, 2026 19:53
Show Gist options
  • Select an option

  • Save bearlikelion/4e0393ed575df27c41a9116181b490a4 to your computer and use it in GitHub Desktop.

Select an option

Save bearlikelion/4e0393ed575df27c41a9116181b490a4 to your computer and use it in GitHub Desktop.
Organize Extracted Everquest 1 Characters
#!/usr/bin/env python3
"""Organize EverQuest character exports into Race/Gender folders and rename animations."""
import struct
import json
import os
import shutil
BASE = os.path.dirname(os.path.abspath(__file__))
TEXTURES_DIR = os.path.join(BASE, "Textures")
# --- Race code mapping ---
# prefix -> (Race, Gender)
RACE_MAP = {
"baf": ("Barbarian", "Female"), "bam": ("Barbarian", "Male"),
"daf": ("DarkElf", "Female"), "dam": ("DarkElf", "Male"),
"dwf": ("Dwarf", "Female"), "dwm": ("Dwarf", "Male"),
"elf": ("WoodElf", "Female"), "elm": ("WoodElf", "Male"),
"erf": ("Erudite", "Female"), "erm": ("Erudite", "Male"),
"gnf": ("Gnome", "Female"), "gnm": ("Gnome", "Male"),
"haf": ("HalfElf", "Female"), "ham": ("HalfElf", "Male"),
"hif": ("HighElf", "Female"), "him": ("HighElf", "Male"),
"hof": ("Halfling", "Female"), "hom": ("Halfling", "Male"),
"huf": ("Human", "Female"), "hum": ("Human", "Male"),
"ogf": ("Ogre", "Female"), "ogm": ("Ogre", "Male"),
"trf": ("Troll", "Female"), "trm": ("Troll", "Male"),
}
# Special models -> Other/{Name}
SPECIAL_MAP = {
"ske": "Skeleton",
"wer": "Werewolf",
"ele": "Erudite_Old",
"boat": "Boat",
"eye": "Eye",
"ivm": "InvisibleMan",
"woe": "Wolf",
}
# Texture prefixes that go to special model folders
SPECIAL_TEX_PREFIXES = ["ske", "wer", "ele", "woe", "eye", "ivm", "boat", "binside"]
# Shared texture prefixes (checked AFTER race-specific cloak check)
SHARED_TEX_PREFIXES = ["clk", "chain", "helm", "magecap"]
# --- Animation name mapping ---
ANIM_MAP = {
"c01": "Kick", "c02": "Primary Jab", "c03": "2H Swipe",
"c04": "2H Jab", "c05": "Primary Swipe", "c06": "Secondary Swipe",
"c07": "Secondary Bash", "c08": "Primary Upper Jab", "c09": "Shoot Arrow",
"c10": "Swim Attack", "c11": "Round Kick",
"d01": "Take Damage", "d02": "Take Damage 2",
"d03": "Swim Take Damage", "d04": "Drowning", "d05": "Death",
"l01": "Walk", "l02": "Run", "l03": "Jump Across", "l04": "Jump",
"l05": "Free Fall", "l06": "Crouch Walk", "l07": "Climb",
"l08": "Crouch", "l09": "Swim Idle",
"o01": "Idle 1", "o02": "Idle 3", "o03": "Sit Idle",
"p01": "Stand", "p02": "Sit", "p03": "Turn Right",
"p04": "Ice Strafe", "p05": "Kneel", "p06": "Swim Forward",
"p07": "Unknown P07", "p08": "Idle 2", "pos": "Default", "drf": "Pose",
"s01": "Cheer", "s02": "Cry", "s03": "Wave", "s04": "Rude",
"s05": "Yawn", "s06": "Nod", "s07": "Amaze", "s08": "Plead",
"s09": "Clap", "s10": "Hungry", "s11": "Blush", "s12": "Chuckle",
"s13": "Cough", "s14": "Duck", "s15": "Puzzle", "s16": "Dance",
"s17": "Blink", "s18": "Glare", "s19": "Drool", "s20": "Kneel Emote",
"s21": "Laugh", "s22": "Point", "s23": "Shrug", "s24": "Ready",
"s25": "Salute", "s26": "Shiver", "s27": "Tap", "s28": "Bow",
"t01": "Instrument Percussion", "t02": "Instrument Strings",
"t03": "Instrument Brass", "t04": "Spell Defense", "t05": "Spell General",
"t06": "Spell Offense", "t07": "Flying Kick",
"t08": "Combo Swipes", "t09": "Kung Fu Jab",
}
def rename_animations_in_glb(filepath):
"""Rewrite a GLB file with renamed animations. Returns count of renamed animations."""
with open(filepath, "rb") as f:
data = f.read()
# GLB header: magic(4) + version(4) + length(4)
if len(data) < 12:
return 0
magic, version, total_length = struct.unpack_from("<III", data, 0)
if magic != 0x46546C67: # 'glTF'
print(f" WARNING: {filepath} is not a valid GLB, skipping")
return 0
# JSON chunk header: length(4) + type(4)
json_chunk_length, json_chunk_type = struct.unpack_from("<II", data, 12)
if json_chunk_type != 0x4E4F534A: # 'JSON'
print(f" WARNING: {filepath} first chunk is not JSON, skipping")
return 0
json_bytes = data[20 : 20 + json_chunk_length]
gltf = json.loads(json_bytes)
# Rename animations
renamed = 0
for anim in gltf.get("animations", []):
old_name = anim.get("name", "")
if old_name in ANIM_MAP:
anim["name"] = ANIM_MAP[old_name]
renamed += 1
if renamed == 0:
return 0
# Serialize new JSON
new_json = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
# Pad to 4-byte boundary with spaces (glTF spec)
padding = (4 - len(new_json) % 4) % 4
new_json_padded = new_json + b" " * padding
new_json_chunk_length = len(new_json_padded)
# Binary chunk (everything after the original JSON chunk)
bin_chunk_start = 20 + json_chunk_length
bin_chunk_data = data[bin_chunk_start:]
# New total length
new_total_length = 12 + 8 + new_json_chunk_length + len(bin_chunk_data)
# Write new GLB
with open(filepath, "wb") as f:
f.write(struct.pack("<III", magic, version, new_total_length))
f.write(struct.pack("<II", new_json_chunk_length, json_chunk_type))
f.write(new_json_padded)
f.write(bin_chunk_data)
return renamed
def get_glb_dest(filename):
"""Determine destination folder for a GLB file. Returns (dest_dir, is_special)."""
stem = filename.replace(".glb", "")
# Strip variant suffix like _00, _01
prefix = stem.split("_")[0]
if prefix in RACE_MAP:
race, gender = RACE_MAP[prefix]
return os.path.join(BASE, race, gender), False
if prefix in SPECIAL_MAP:
return os.path.join(BASE, "Other", SPECIAL_MAP[prefix]), True
print(f" WARNING: Unknown GLB prefix '{prefix}' for {filename}, skipping")
return None, False
def get_texture_dest(filename):
"""Determine destination folder for a texture file. Returns dest_dir or None."""
lower = filename.lower()
# Race-specific cloak textures: clkerf*.png, clkerm*.png
if lower.startswith("clkerf"):
return os.path.join(BASE, "Erudite", "Female", "Textures")
if lower.startswith("clkerm"):
return os.path.join(BASE, "Erudite", "Male", "Textures")
# Special model textures
for sp in SPECIAL_TEX_PREFIXES:
if lower.startswith(sp):
if sp == "binside":
return os.path.join(BASE, "Other", "Boat", "Textures")
if sp == "boat":
return os.path.join(BASE, "Other", "Boat", "Textures")
if sp in SPECIAL_MAP:
return os.path.join(BASE, "Other", SPECIAL_MAP[sp], "Textures")
return None
# Shared textures
for sp in SHARED_TEX_PREFIXES:
if lower.startswith(sp):
return os.path.join(BASE, "Shared", "Textures")
# Race textures: first 3 chars are the race code
prefix = lower[:3]
if prefix in RACE_MAP:
race, gender = RACE_MAP[prefix]
return os.path.join(BASE, race, gender, "Textures")
print(f" WARNING: Unknown texture prefix for {filename}, skipping")
return None
def main():
print("=== Organizing EverQuest Character Exports ===\n")
# --- Step 1: Move GLB files ---
print("--- Moving GLB files ---")
glb_files = sorted(f for f in os.listdir(BASE) if f.endswith(".glb"))
glb_moved = 0
for glb in glb_files:
dest_dir, _ = get_glb_dest(glb)
if dest_dir is None:
continue
os.makedirs(dest_dir, exist_ok=True)
src = os.path.join(BASE, glb)
dst = os.path.join(dest_dir, glb)
shutil.move(src, dst)
glb_moved += 1
print(f" Moved {glb_moved} GLB files\n")
# --- Step 2: Move textures ---
print("--- Moving texture files ---")
tex_moved = 0
if os.path.isdir(TEXTURES_DIR):
tex_files = sorted(os.listdir(TEXTURES_DIR))
for tex in tex_files:
if not tex.endswith(".png"):
continue
dest_dir = get_texture_dest(tex)
if dest_dir is None:
continue
os.makedirs(dest_dir, exist_ok=True)
src = os.path.join(TEXTURES_DIR, tex)
dst = os.path.join(dest_dir, tex)
shutil.move(src, dst)
tex_moved += 1
print(f" Moved {tex_moved} texture files\n")
# Clean up empty Textures directory
remaining = os.listdir(TEXTURES_DIR)
if not remaining:
os.rmdir(TEXTURES_DIR)
print(" Removed empty Textures/ directory\n")
else:
print(f" WARNING: {len(remaining)} files remain in Textures/: {remaining[:5]}...\n")
else:
print(" No Textures/ directory found\n")
# --- Step 3: Rename animations in all GLB files ---
print("--- Renaming animations in GLB files ---")
total_renamed = 0
glb_count = 0
for root, dirs, files in os.walk(BASE):
for f in sorted(files):
if not f.endswith(".glb"):
continue
path = os.path.join(root, f)
renamed = rename_animations_in_glb(path)
if renamed > 0:
rel = os.path.relpath(path, BASE)
print(f" {rel}: {renamed} animations renamed")
total_renamed += renamed
glb_count += 1
print(f"\n Total: {total_renamed} animations renamed across {glb_count} GLB files\n")
# --- Step 4: Clean up script ---
script_path = os.path.join(BASE, "organize_characters.py")
if os.path.exists(script_path):
os.remove(script_path)
print("--- Cleaned up: removed organize_characters.py ---\n")
print("=== Done! ===")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment