#!/usr/bin/env python3 # Disassemble a cutscene script from overlayhelpers import filemap import argparse, os, struct import math """ Enumerations """ cscam_continue_stop = { 0x00: "CS_CAM_CONTINUE", 0xFF: "CS_CAM_STOP", } cutscene_destinations = { 0x00: "CS_DEST_UNIMPLEMENTED_0", 0x01: "CS_DEST_CUTSCENE_MAP_GANON_HORSE", 0x02: "CS_DEST_CUTSCENE_MAP_THREE_GODDESSES", 0x03: "CS_DEST_GERUDO_VALLEY_DIN_PART_1", 0x04: "CS_DEST_DEATH_MOUNTAIN_TRAIL_NAYRU", 0x05: "CS_DEST_KOKIRI_FOREST_FARORE", 0x06: "CS_DEST_CUTSCENE_MAP_TRIFORCE_CREATION", 0x07: "CS_DEST_KOKIRI_FOREST_RECEIVE_KOKIRI_EMERALD", 0x08: "CS_DEST_TEMPLE_OF_TIME_FROM_MASTER_SWORD", 0x09: "CS_DEST_GERUDO_VALLEY_DIN_PART_2", 0x0A: "CS_DEST_LINKS_HOUSE_INTRO", 0x0B: "CS_DEST_KOKIRI_FOREST_INTRO", 0x0C: "CS_DEST_DEATH_MOUNTAIN_TRAIL_FROM_GORON_RUBY", 0x0D: "CS_DEST_ZORAS_FOUNTAIN_FROM_ZORAS_SAPPHIRE", 0x0E: "CS_DEST_KOKIRI_FOREST_FROM_KOKIRI_EMERALD", 0x0F: "CS_DEST_TEMPLE_OF_TIME_KOKIRI_EMERALD_RESTORED", 0x10: "CS_DEST_TEMPLE_OF_TIME_GORON_RUBY_RESTORED", 0x11: "CS_DEST_TEMPLE_OF_TIME_ZORAS_SAPPHIRE_RESTORED", 0x12: "CS_DEST_TEMPLE_OF_TIME_AFTER_LIGHT_MEDALLION", 0x13: "CS_DEST_DEATH_MOUNTAIN_TRAIL", 0x14: "CS_DEST_UNIMPLEMENTED_14", 0x15: "CS_DEST_LAKE_HYLIA_WATER_RESTORED", 0x16: "CS_DEST_DESERT_COLOSSUS_REQUIEM", 0x17: "CS_DEST_CUTSCENE_MAP_GANONDORF_DEFEATED_CREDITS", 0x18: "CS_DEST_JABU_JABU", 0x19: "CS_DEST_CHAMBER_OF_SAGES_LIGHT_MEDALLION", 0x1A: "CS_DEST_TEMPLE_OF_TIME_KOKIRI_EMERALD_RESTORED_2", 0x1B: "CS_DEST_TEMPLE_OF_TIME_GORON_RUBY_RESTORED_2", 0x1C: "CS_DEST_TEMPLE_OF_TIME_ZORAS_SAPPHIRE_RESTORED_2", 0x1D: "CS_DEST_CHAMBER_OF_SAGES_FOREST_MEDALLION", 0x1E: "CS_DEST_CHAMBER_OF_SAGES_FIRE_MEDALLION", 0x1F: "CS_DEST_CHAMBER_OF_SAGES_WATER_MEDALLION", 0x20: "CS_DEST_HYRULE_FIELD_FLASHBACK", 0x21: "CS_DEST_HYRULE_FIELD_FROM_ZELDA_ESCAPE", 0x22: "CS_DEST_CUTSCENE_MAP_GANONDORF_FROM_MASTER_SWORD", 0x23: "CS_DEST_HYRULE_FIELD_INTRO_DREAM", 0x24: "CS_DEST_UNIMPLEMENTED_24", 0x25: "CS_DEST_UNIMPLEMENTED_25", 0x26: "CS_DEST_CUTSCENE_MAP_SHEIKAH_LEGEND", 0x27: "CS_DEST_TEMPLE_OF_TIME_ZELDA_REVEAL", 0x28: "CS_DEST_TEMPLE_OF_TIME_GET_LIGHT_ARROWS", 0x29: "CS_DEST_LAKE_HYLIA_FROM_LAKE_RESTORED", 0x2A: "CS_DEST_KAKARIKO_VILLAGE_DRAIN_WELL", 0x2B: "CS_DEST_WINDMILL_FROM_WELL_DRAINED", 0x2C: "CS_DEST_TEMPLE_OF_TIME_FROM_ALL_STONES_RESTORED", 0x2D: "CS_DEST_UNIMPLEMENTED_2D", 0x2E: "CS_DEST_TEMPLE_OF_TIME_AFTER_LIGHT_MEDALLION_ALT", 0x2F: "CS_DEST_KAKARIKO_VILLAGE_NOCTURNE_PART_2", 0x30: "CS_DEST_DESERT_COLOSSUS_FROM_REQUIEM", 0x31: "CS_DEST_TEMPLE_OF_TIME_FROM_LIGHT_ARROWS", 0x32: "CS_DEST_KAKARIKO_VILLAGE_FROM_NOCTURNE", 0x33: "CS_DEST_HYRULE_FIELD_FROM_ZELDAS_COURTYARD", 0x34: "CS_DEST_TEMPLE_OF_TIME_SONG_OF_TIME", 0x35: "CS_DEST_HYRULE_FIELD_FROM_SONG_OF_TIME", 0x36: "CS_DEST_GERUDO_VALLEY_CREDITS", 0x37: "CS_DEST_GERUDO_FORTRESS_CREDITS", 0x38: "CS_DEST_KAKARIKO_VILLAGE_CREDITS", 0x39: "CS_DEST_DEATH_MOUNTAIN_TRAIL_CREDITS_PART_1", 0x3A: "CS_DEST_GORON_CITY_CREDITS", 0x3B: "CS_DEST_LAKE_HYLIA_CREDITS", 0x3C: "CS_DEST_ZORAS_FOUNTAIN_CREDITS", 0x3D: "CS_DEST_ZORAS_DOMAIN_CREDITS", 0x3E: "CS_DEST_KOKIRI_FOREST_CREDITS_PART_1", 0x3F: "CS_DEST_KOKIRI_FOREST_CREDITS_PART_2", 0x40: "CS_DEST_HYRULE_FIELD_CREDITS", 0x41: "CS_DEST_LON_LON_RANCH_CREDITS_PART_1_ALT", 0x42: "CS_DEST_KAKARIKO_VILLAGE_FROM_TRAIL_OWL", 0x43: "CS_DEST_HYRULE_FIELD_FROM_LAKE_HYLIA_OWL", 0x44: "CS_DEST_CUTSCENE_MAP_DEKU_SPROUT_PART_2", 0x45: "CS_DEST_KOKIRI_FOREST_DEKU_SPROUT_PART_3", 0x46: "CS_DEST_DEATH_MOUNTAIN_TRAIL_CREDITS_PART_2", 0x47: "CS_DEST_TEMPLE_OF_TIME_CREDITS", 0x48: "CS_DEST_ZELDAS_COURTYARD_CREDITS", 0x49: "CS_DEST_LON_LON_RANCH_CREDITS_PART_1", 0x4A: "CS_DEST_LON_LON_RANCH_CREDITS_PART_2", 0x4B: "CS_DEST_LON_LON_RANCH_CREDITS_PART_3", 0x4C: "CS_DEST_LON_LON_RANCH_CREDITS_PART_4", 0x4D: "CS_DEST_LON_LON_RANCH_CREDITS_PART_5", 0x4E: "CS_DEST_LON_LON_RANCH_CREDITS_PART_6", 0x4F: "CS_DEST_LON_LON_RANCH_1", 0x50: "CS_DEST_LON_LON_RANCH_2", 0x51: "CS_DEST_LON_LON_RANCH_3", 0x52: "CS_DEST_LON_LON_RANCH_4", 0x53: "CS_DEST_LON_LON_RANCH_5", 0x54: "CS_DEST_LON_LON_RANCH_6", 0x55: "CS_DEST_LON_LON_RANCH_7", 0x56: "CS_DEST_LON_LON_RANCH_8", 0x57: "CS_DEST_LON_LON_RANCH_9", 0x58: "CS_DEST_LON_LON_RANCH_10", 0x59: "CS_DEST_LON_LON_RANCH_11", 0x5A: "CS_DEST_LON_LON_RANCH_12", 0x5B: "CS_DEST_LON_LON_RANCH_13", 0x5C: "CS_DEST_LON_LON_RANCH_14", 0x5D: "CS_DEST_LON_LON_RANCH_15", 0x5E: "CS_DEST_LON_LON_RANCH_FROM_EPONAS_SONG", 0x5F: "CS_DEST_STONES_RESTORED_CONDITIONAL", 0x60: "CS_DEST_DESERT_COLOSSUS_FROM_CHAMBER_OF_SAGES", 0x61: "CS_DEST_GRAVEYARD_FROM_CHAMBER_OF_SAGES", 0x62: "CS_DEST_DEATH_MOUNTAIN_CRATER_FROM_CHAMBER_OF_SAGES", 0x63: "CS_DEST_SACRED_FOREST_MEADOW_WARP_PAD", 0x64: "CS_DEST_KOKIRI_FOREST_FROM_CHAMBER_OF_SAGES", 0x65: "CS_DEST_DESERT_COLOSSUS_FROM_NABOORU_CAPTURE", 0x66: "CS_DEST_TEMPLE_OF_TIME_FRONT_OF_PEDESTAL", 0x67: "CS_DEST_HYRULE_FIELD_TITLE_SCREEN", 0x68: "CS_DEST_TITLE_SCREEN_DEMO", 0x69: "CS_DEST_GRAVEYARD_SUNS_SONG_PART_2", 0x6A: "CS_DEST_ROYAL_FAMILYS_TOMB_SUNS_SONG_PART_3", 0x6B: "CS_DEST_GANONS_CASTLE_DISPEL_FOREST_BEAM", 0x6C: "CS_DEST_GANONS_CASTLE_DISPEL_WATER_BEAM", 0x6D: "CS_DEST_GANONS_CASTLE_DISPEL_SHADOW_BEAM", 0x6E: "CS_DEST_GANONS_CASTLE_DISPEL_FIRE_BEAM", 0x6F: "CS_DEST_GANONS_CASTLE_DISPEL_LIGHT_BEAM", 0x70: "CS_DEST_GANONS_CASTLE_DISPEL_SPIRIT_BEAM", 0x71: "CS_DEST_GANONS_CASTLE_DISPEL_BARRIER_CONDITONAL", 0x72: "CS_DEST_HYRULE_FIELD_FROM_FAIRY_OCARINA", 0x73: "CS_DEST_HYRULE_FIELD_FROM_IMPA_ESCORT", 0x74: "CS_DEST_FROM_RAURU_FINAL_MESSAGE_CONDITIONAL", 0x75: "CS_DEST_HYRULE_FIELD_CREDITS_SKY", 0x76: "CS_DEST_GANON_BATTLE_TOWER_COLLAPSE", 0x77: "CS_DEST_ZELDAS_COURTYARD_RECEIVE_LETTER", } ocarina_action_ids = { 0x00: "OCARINA_ACTION_UNK_0", 0x01: "OCARINA_ACTION_FREE_PLAY", 0x02: "OCARINA_ACTION_TEACH_MINUET", 0x03: "OCARINA_ACTION_TEACH_BOLERO", 0x04: "OCARINA_ACTION_TEACH_SERENADE", 0x05: "OCARINA_ACTION_TEACH_REQUIEM", 0x06: "OCARINA_ACTION_TEACH_NOCTURNE", 0x07: "OCARINA_ACTION_TEACH_PRELUDE", 0x08: "OCARINA_ACTION_TEACH_SARIA", 0x09: "OCARINA_ACTION_TEACH_EPONA", 0x0A: "OCARINA_ACTION_TEACH_LULLABY", 0x0B: "OCARINA_ACTION_TEACH_SUNS", 0x0C: "OCARINA_ACTION_TEACH_TIME", 0x0D: "OCARINA_ACTION_TEACH_STORMS", 0x0E: "OCARINA_ACTION_UNK_E", 0x0F: "OCARINA_ACTION_PLAYBACK_MINUET", 0x10: "OCARINA_ACTION_PLAYBACK_BOLERO", 0x11: "OCARINA_ACTION_PLAYBACK_SERENADE", 0x12: "OCARINA_ACTION_PLAYBACK_REQUIEM", 0x13: "OCARINA_ACTION_PLAYBACK_NOCTURNE", 0x14: "OCARINA_ACTION_PLAYBACK_PRELUDE", 0x15: "OCARINA_ACTION_PLAYBACK_SARIA", 0x16: "OCARINA_ACTION_PLAYBACK_EPONA", 0x17: "OCARINA_ACTION_PLAYBACK_LULLABY", 0x18: "OCARINA_ACTION_PLAYBACK_SUNS", 0x19: "OCARINA_ACTION_PLAYBACK_TIME", 0x1A: "OCARINA_ACTION_PLAYBACK_STORMS", 0x1B: "OCARINA_ACTION_UNK_1B", 0x1C: "OCARINA_ACTION_CHECK_MINUET", 0x1D: "OCARINA_ACTION_CHECK_BOLERO", 0x1E: "OCARINA_ACTION_CHECK_SERENADE", 0x1F: "OCARINA_ACTION_CHECK_REQUIEM", 0x20: "OCARINA_ACTION_CHECK_NOCTURNE", 0x21: "OCARINA_ACTION_CHECK_PRELUDE", 0x22: "OCARINA_ACTION_CHECK_SARIA", 0x23: "OCARINA_ACTION_CHECK_EPONA", 0x24: "OCARINA_ACTION_CHECK_LULLABY", 0x25: "OCARINA_ACTION_CHECK_SUNS", 0x26: "OCARINA_ACTION_CHECK_TIME", 0x27: "OCARINA_ACTION_CHECK_STORMS", 0x28: "OCARINA_ACTION_CHECK_SCARECROW_SPAWN", 0x29: "OCARINA_ACTION_FREE_PLAY_DONE", 0x2A: "OCARINA_ACTION_SCARECROW_LONG_RECORDING", 0x2B: "OCARINA_ACTION_SCARECROW_LONG_PLAYBACK", 0x2C: "OCARINA_ACTION_SCARECROW_SPAWN_RECORDING", 0x2D: "OCARINA_ACTION_SCARECROW_SPAWN_PLAYBACK", 0x2E: "OCARINA_ACTION_MEMORY_GAME", 0x2F: "OCARINA_ACTION_FROGS", 0x30: "OCARINA_ACTION_CHECK_NOWARP", 0x31: "OCARINA_ACTION_CHECK_NOWARP_DONE", } sequence_ids = { 0x00: "NA_BGM_GENERAL_SFX", 0x01: "NA_BGM_NATURE_AMBIENCE", 0x02: "NA_BGM_FIELD_LOGIC", 0x03: "NA_BGM_FIELD_INIT", 0x04: "NA_BGM_FIELD_DEFAULT_1", 0x05: "NA_BGM_FIELD_DEFAULT_2", 0x06: "NA_BGM_FIELD_DEFAULT_3", 0x07: "NA_BGM_FIELD_DEFAULT_4", 0x08: "NA_BGM_FIELD_DEFAULT_5", 0x09: "NA_BGM_FIELD_DEFAULT_6", 0x0A: "NA_BGM_FIELD_DEFAULT_7", 0x0B: "NA_BGM_FIELD_DEFAULT_8", 0x0C: "NA_BGM_FIELD_DEFAULT_9", 0x0D: "NA_BGM_FIELD_DEFAULT_A", 0x0E: "NA_BGM_FIELD_DEFAULT_B", 0x0F: "NA_BGM_FIELD_ENEMY_INIT", 0x10: "NA_BGM_FIELD_ENEMY_1", 0x11: "NA_BGM_FIELD_ENEMY_2", 0x12: "NA_BGM_FIELD_ENEMY_3", 0x13: "NA_BGM_FIELD_ENEMY_4", 0x14: "NA_BGM_FIELD_STILL_1", 0x15: "NA_BGM_FIELD_STILL_2", 0x16: "NA_BGM_FIELD_STILL_3", 0x17: "NA_BGM_FIELD_STILL_4", 0x18: "NA_BGM_DUNGEON", 0x19: "NA_BGM_KAKARIKO_ADULT", 0x1A: "NA_BGM_ENEMY", 0x1B: "NA_BGM_BOSS", 0x1C: "NA_BGM_INSIDE_DEKU_TREE", 0x1D: "NA_BGM_MARKET", 0x1E: "NA_BGM_TITLE", 0x1F: "NA_BGM_LINK_HOUSE", 0x20: "NA_BGM_GAME_OVER", 0x21: "NA_BGM_BOSS_CLEAR", 0x22: "NA_BGM_ITEM_GET", 0x23: "NA_BGM_OPENING_GANON", 0x24: "NA_BGM_HEART_GET", 0x25: "NA_BGM_OCA_LIGHT", 0x26: "NA_BGM_JABU_JABU", 0x27: "NA_BGM_KAKARIKO_KID", 0x28: "NA_BGM_GREAT_FAIRY", 0x29: "NA_BGM_ZELDA_THEME", 0x2A: "NA_BGM_FIRE_TEMPLE", 0x2B: "NA_BGM_OPEN_TRE_BOX", 0x2C: "NA_BGM_FOREST_TEMPLE", 0x2D: "NA_BGM_COURTYARD", 0x2E: "NA_BGM_GANON_TOWER", 0x2F: "NA_BGM_LONLON", 0x30: "NA_BGM_GORON_CITY", 0x31: "NA_BGM_FIELD_MORNING", 0x32: "NA_BGM_SPIRITUAL_STONE", 0x33: "NA_BGM_OCA_BOLERO", 0x34: "NA_BGM_OCA_MINUET", 0x35: "NA_BGM_OCA_SERENADE", 0x36: "NA_BGM_OCA_REQUIEM", 0x37: "NA_BGM_OCA_NOCTURNE", 0x38: "NA_BGM_MINI_BOSS", 0x39: "NA_BGM_SMALL_ITEM_GET", 0x3A: "NA_BGM_TEMPLE_OF_TIME", 0x3B: "NA_BGM_EVENT_CLEAR", 0x3C: "NA_BGM_KOKIRI", 0x3D: "NA_BGM_OCA_FAIRY_GET", 0x3E: "NA_BGM_SARIA_THEME", 0x3F: "NA_BGM_SPIRIT_TEMPLE", 0x40: "NA_BGM_HORSE", 0x41: "NA_BGM_HORSE_GOAL", 0x42: "NA_BGM_INGO", 0x43: "NA_BGM_MEDALLION_GET", 0x44: "NA_BGM_OCA_SARIA", 0x45: "NA_BGM_OCA_EPONA", 0x46: "NA_BGM_OCA_ZELDA", 0x47: "NA_BGM_OCA_SUNS", 0x48: "NA_BGM_OCA_TIME", 0x49: "NA_BGM_OCA_STORM", 0x4A: "NA_BGM_NAVI_OPENING", 0x4B: "NA_BGM_DEKU_TREE_CS", 0x4C: "NA_BGM_WINDMILL", 0x4D: "NA_BGM_HYRULE_CS", 0x4E: "NA_BGM_MINI_GAME", 0x4F: "NA_BGM_SHEIK", 0x50: "NA_BGM_ZORA_DOMAIN", 0x51: "NA_BGM_APPEAR", 0x52: "NA_BGM_ADULT_LINK", 0x53: "NA_BGM_MASTER_SWORD", 0x54: "NA_BGM_INTRO_GANON", 0x55: "NA_BGM_SHOP", 0x56: "NA_BGM_CHAMBER_OF_SAGES", 0x57: "NA_BGM_FILE_SELECT", 0x58: "NA_BGM_ICE_CAVERN", 0x59: "NA_BGM_DOOR_OF_TIME", 0x5A: "NA_BGM_OWL", 0x5B: "NA_BGM_SHADOW_TEMPLE", 0x5C: "NA_BGM_WATER_TEMPLE", 0x5D: "NA_BGM_BRIDGE_TO_GANONS", 0x5E: "NA_BGM_OCARINA_OF_TIME", 0x5F: "NA_BGM_GERUDO_VALLEY", 0x60: "NA_BGM_POTION_SHOP", 0x61: "NA_BGM_KOTAKE_KOUME", 0x62: "NA_BGM_ESCAPE", 0x63: "NA_BGM_UNDERGROUND", 0x64: "NA_BGM_GANONDORF_BOSS", 0x65: "NA_BGM_GANON_BOSS", 0x66: "NA_BGM_END_DEMO", 0x67: "NA_BGM_STAFF_1", 0x68: "NA_BGM_STAFF_2", 0x69: "NA_BGM_STAFF_3", 0x6A: "NA_BGM_STAFF_4", 0x6B: "NA_BGM_FIRE_BOSS", 0x6C: "NA_BGM_TIMED_MINI_GAME", 0x6D: "NA_BGM_CUTSCENE_EFFECTS", 0x7F: "NA_BGM_NO_MUSIC", 0x80: "NA_BGM_NATURE_SFX_RAIN", 0xFFFF: "NA_BGM_DISABLED", } cutscene_misc_types = { 0x00: "CS_MISC_UNIMPLEMENTED_0", 0x01: "CS_MISC_RAIN", 0x02: "CS_MISC_LIGHTNING", 0x03: "CS_MISC_SET_CSFLAG_0", 0x04: "CS_MISC_UNIMPLEMENTED_4", 0x05: "CS_MISC_UNIMPLEMENTED_5", 0x06: "CS_MISC_LIFT_FOG", 0x07: "CS_MISC_CLOUDY_SKY", 0x08: "CS_MISC_FADE_KOKIRI_GRASS_ENV_ALPHA", 0x09: "CS_MISC_SNOW", 0x0A: "CS_MISC_SET_CSFLAG_1", 0x0B: "CS_MISC_DEKU_TREE_DEATH", 0x0C: "CS_MISC_STOP_CUTSCENE", 0x0D: "CS_MISC_TRIFORCE_FLASH", 0x0E: "CS_MISC_SET_LOCKED_VIEWPOINT", 0x0F: "CS_MISC_SHOW_TITLE_CARD", 0x10: "CS_MISC_QUAKE_START", 0x11: "CS_MISC_QUAKE_STOP", 0x12: "CS_MISC_STOP_STORM_AND_ADVANCE_TO_DAY", 0x13: "CS_MISC_SET_FLAG_FAST_WINDMILL", 0x14: "CS_MISC_SET_FLAG_WELL_DRAINED", 0x15: "CS_MISC_SET_FLAG_LAKE_HYLIA_RESTORED", 0x16: "CS_MISC_VISMONO_BLACK_AND_WHITE", 0x17: "CS_MISC_VISMONO_SEPIA", 0x18: "CS_MISC_HIDE_ROOM", 0x19: "CS_MISC_TIME_ADVANCE_TO_NIGHT", 0x1A: "CS_MISC_SET_TIME_BASED_LIGHT_SETTING", 0x1B: "CS_MISC_RED_PULSATING_LIGHTS", 0x1C: "CS_MISC_HALT_ALL_ACTORS", 0x1D: "CS_MISC_RESUME_ALL_ACTORS", 0x1E: "CS_MISC_SET_CSFLAG_3", 0x1F: "CS_MISC_SET_CSFLAG_4", 0x20: "CS_MISC_SANDSTORM_FILL", 0x21: "CS_MISC_SUNSSONG_START", 0x22: "CS_MISC_FREEZE_TIME", 0x23: "CS_MISC_LONG_SCARECROW_SONG", } cutscene_transition_types = { 0x01: "CS_TRANS_GRAY_FILL_IN", 0x02: "CS_TRANS_BLUE_FILL_IN", 0x03: "CS_TRANS_RED_FILL_OUT", 0x04: "CS_TRANS_GREEN_FILL_OUT", 0x05: "CS_TRANS_GRAY_FILL_OUT", 0x06: "CS_TRANS_BLUE_FILL_OUT", 0x07: "CS_TRANS_RED_FILL_IN", 0x08: "CS_TRANS_GREEN_FILL_IN", 0x09: "CS_TRANS_TRIGGER_INSTANCE", 0x0A: "CS_TRANS_BLACK_FILL_OUT", 0x0B: "CS_TRANS_BLACK_FILL_IN", 0x0C: "CS_TRANS_BLACK_FILL_OUT_TO_HALF", 0x0D: "CS_TRANS_BLACK_FILL_IN_FROM_HALF", } player_cue_ids = { 0x00: "PLAYER_CUEID_NONE", 0x01: "PLAYER_CUEID_1", 0x02: "PLAYER_CUEID_2", 0x03: "PLAYER_CUEID_3", 0x04: "PLAYER_CUEID_4", 0x05: "PLAYER_CUEID_5", 0x06: "PLAYER_CUEID_6", 0x07: "PLAYER_CUEID_7", 0x08: "PLAYER_CUEID_8", 0x09: "PLAYER_CUEID_9", 0x0A: "PLAYER_CUEID_10", 0x0B: "PLAYER_CUEID_11", 0x0C: "PLAYER_CUEID_12", 0x0D: "PLAYER_CUEID_13", 0x0E: "PLAYER_CUEID_14", 0x0F: "PLAYER_CUEID_15", 0x10: "PLAYER_CUEID_16", 0x11: "PLAYER_CUEID_17", 0x12: "PLAYER_CUEID_18", 0x13: "PLAYER_CUEID_19", 0x14: "PLAYER_CUEID_20", 0x15: "PLAYER_CUEID_21", 0x16: "PLAYER_CUEID_22", 0x17: "PLAYER_CUEID_23", 0x18: "PLAYER_CUEID_24", 0x19: "PLAYER_CUEID_25", 0x1A: "PLAYER_CUEID_26", 0x1B: "PLAYER_CUEID_27", 0x1C: "PLAYER_CUEID_28", 0x1D: "PLAYER_CUEID_29", 0x1E: "PLAYER_CUEID_30", 0x1F: "PLAYER_CUEID_31", 0x20: "PLAYER_CUEID_32", 0x21: "PLAYER_CUEID_33", 0x22: "PLAYER_CUEID_34", 0x23: "PLAYER_CUEID_35", 0x24: "PLAYER_CUEID_36", 0x25: "PLAYER_CUEID_37", 0x26: "PLAYER_CUEID_38", 0x27: "PLAYER_CUEID_39", 0x28: "PLAYER_CUEID_40", 0x29: "PLAYER_CUEID_41", 0x2A: "PLAYER_CUEID_42", 0x2B: "PLAYER_CUEID_43", 0x2C: "PLAYER_CUEID_44", 0x2D: "PLAYER_CUEID_45", 0x2E: "PLAYER_CUEID_46", 0x2F: "PLAYER_CUEID_47", 0x30: "PLAYER_CUEID_48", 0x31: "PLAYER_CUEID_49", 0x32: "PLAYER_CUEID_50", 0x33: "PLAYER_CUEID_51", 0x34: "PLAYER_CUEID_52", 0x35: "PLAYER_CUEID_53", 0x36: "PLAYER_CUEID_54", 0x37: "PLAYER_CUEID_55", 0x38: "PLAYER_CUEID_56", 0x39: "PLAYER_CUEID_57", 0x3A: "PLAYER_CUEID_58", 0x3B: "PLAYER_CUEID_59", 0x3C: "PLAYER_CUEID_60", 0x3D: "PLAYER_CUEID_61", 0x3E: "PLAYER_CUEID_62", 0x3F: "PLAYER_CUEID_63", 0x40: "PLAYER_CUEID_64", 0x41: "PLAYER_CUEID_65", 0x42: "PLAYER_CUEID_66", 0x43: "PLAYER_CUEID_67", 0x44: "PLAYER_CUEID_68", 0x45: "PLAYER_CUEID_69", 0x46: "PLAYER_CUEID_70", 0x47: "PLAYER_CUEID_71", 0x48: "PLAYER_CUEID_72", 0x49: "PLAYER_CUEID_73", 0x4A: "PLAYER_CUEID_74", 0x4B: "PLAYER_CUEID_75", 0x4C: "PLAYER_CUEID_76", 0x4D: "PLAYER_CUEID_77", 0x4E: "PLAYER_CUEID_MAX", } fade_out_seq_player = { 0x03: "CS_FADE_OUT_FANFARE", 0x04: "CS_FADE_OUT_BGM_MAIN", } """ Entry format: (n1, n2, n3, ...) : (cmd, nwords, continuestopbyte, cmdentrieswd, listitem, nwordslist) n1, n2, n3, ... : command types cmd : command macro nwords : number of words this command is encoded in continuestopbyte : the location of the continue/stop flag byte, if this command type has one, counted as number of bytes into the command cmdentrieswd : the word index (from 0) containing the number of list items, a negative value specifies a hardcoded number of entries that are always guaranteed to follow listitem : the list item macro if this macro is a list type, the name __SPECIAL is reserved for conditional output that needs custom handling nwordslist : number of words this list item is encoded in Argument format: %[bn|hn|wn]:[N]:[s|x|en] b : byte h : halfword w : word n: nth byte/halfword/word in the Nth word N : word number to search in, 0 is command type s : decimal u : decimal unsigned x : hex x-1 : hex but write the value read minus 1 f : float e : enumeration n : unique identifier of which enum to use """ cutscene_command_macros = { -1: ("CS_END()", 1, None, None, None, None), 3: ("CS_MISC_LIST(%w1:1:s)", 2, None, 0, "CS_MISC(%h2:1:e4, %h1:1:s, %h2:2:s, %h1:2:x, %w1:3:x, %w1:4:x, %w1:5:x, %w1:6:x, %w1:7:x, %w1:8:x, %w1:9:x, %w1:10:x, %w1:11:x, %w1:12:x)", 12), 4: ("CS_LIGHT_SETTING_LIST(%w1:1:s)", 2, None, 0, "CS_LIGHT_SETTING(%h2:1:x-1, %h1:1:s, %h2:2:s, %h1:2:x, %w1:3:x, %w1:4:x, %w1:5:x, %w1:6:x, %w1:7:x, %w1:8:x, %w1:9:x, %w1:10:x, %w1:11:x, %w1:12:x)", 12), 86: ("CS_START_SEQ_LIST(%w1:1:s)", 2, None, 0, "CS_START_SEQ(%h2:1:e3, %h1:1:s, %h2:2:s, %h1:2:x, %w1:3:x, %w1:4:x, %w1:5:x, %w1:6:x, %w1:7:x, %w1:8:x, %w1:9:x)", 12), 87: ("CS_STOP_SEQ_LIST(%w1:1:s)", 2, None, 0, "CS_STOP_SEQ(%h2:1:e3, %h1:1:s, %h2:2:s, %h1:2:x, %w1:3:x, %w1:4:x, %w1:5:x, %w1:6:x, %w1:7:x, %w1:8:x, %w1:9:x)", 12), 124: ("CS_FADE_OUT_SEQ_LIST(%w1:1:s)", 2, None, 0, "CS_FADE_OUT_SEQ(%h2:1:e7, %h1:1:s, %h2:2:s, %h1:2:x, %w1:3:x, %w1:4:x, %w1:5:x, %w1:6:x, %w1:7:x, %w1:8:x, %w1:9:x)", 12), 9: ("CS_RUMBLE_CONTROLLER_LIST(%w1:1:s)", 2, None, 0, "CS_RUMBLE_CONTROLLER(%h2:1:x, %h1:1:s, %h2:2:s, %b2:2:x, %b1:2:x, %b4:3:x, %b3:3:x, %h1:3:x)", 3), 140: ("CS_TIME_LIST(%w1:1:s)", 2, None, 0, "CS_TIME(%h2:1:x, %h1:1:s, %h2:2:s, %b2:2:x, %b1:2:x, %w1:3:x)", 3), 10: ("CS_PLAYER_CUE_LIST(%w1:1:s)", 2, None, 0, "CS_PLAYER_CUE(%h2:1:e6, %h1:1:s, %h2:2:s, %h1:2:x, %h2:3:x, %h1:3:x, %w1:4:s, %w1:5:s, %w1:6:s, %w1:7:s, %w1:8:s, %w1:9:s, %w1:10:f, %w1:11:f, %w1:12:f)", 12), (15,17,18,23,34,39,46,76,85,93,105,107,110,119,123,138,139,144, # npc action 1 14,16,24,35,40,48,64,68,70,78,80,94,116,118,120,125,131,141, # npc action 2 25,36,41,50,67,69,72,74,81,106,117,121,126,132, # npc action 3 29,37,42,51,53,63,65,66,75,82,108,127,133, # npc action 4 30,38,43,47,54,79,83,128,135, # npc action 5 44,55,77,84,90,129,136, # npc action 6 31,52,57,58,88,115,130,137, # npc action 7 49,60,89,111,114,134,142, # npc action 8 62, # npc action 9 143): # npc action 10 ("CS_ACTOR_CUE_LIST(%w1:0:s, %w1:1:s)", 2, None, 0, "CS_ACTOR_CUE(%h2:1:x, %h1:1:s, %h2:2:s, %h1:2:x, %h2:3:x, %h1:3:x, %w1:4:s, %w1:5:s, %w1:6:s, %w1:7:s, %w1:8:s, %w1:9:s, %w1:10:f, %w1:11:f, %w1:12:f)", 12), 1: ("CS_CAM_EYE_SPLINE(%h1:1:s, %h2:2:s)", 3, 0, None, "CS_CAM_POINT(%b4:1:e0, %b3:1:x, %h1:1:s, %w1:2:f, %h2:3:s, %h1:3:s, %h2:4:s, %h1:4:x)", 4), 5: ("CS_CAM_EYE_SPLINE_REL_TO_PLAYER(%h1:1:s, %h2:2:s)", 3, 0, None, "CS_CAM_POINT(%b4:1:e0, %b3:1:x, %h1:1:s, %w1:2:f, %h2:3:s, %h1:3:s, %h2:4:s, %h1:4:x)", 4), 2: ("CS_CAM_AT_SPLINE(%h1:1:s, %h2:2:s)", 3, 0, None, "CS_CAM_POINT(%b4:1:e0, %b3:1:x, %h1:1:s, %w1:2:f, %h2:3:s, %h1:3:s, %h2:4:s, %h1:4:x)", 4), 6: ("CS_CAM_AT_SPLINE_REL_TO_PLAYER(%h1:1:s, %h2:2:s)", 3, 0, None, "CS_CAM_POINT(%b4:1:e0, %b3:1:x, %h1:1:s, %w1:2:f, %h2:3:s, %h1:3:s, %h2:4:s, %h1:4:x)", 4), 7: ("CS_CAM_EYE_LIST(%h2:1:x, %h1:1:s, %h2:2:s, %h1:2:x)", 3, None, -1, "CS_CAM_POINT(%b4:1:e0, %b3:1:x, %h1:1:s, %w1:2:f, %h2:3:s, %h1:3:s, %h2:4:s, %h1:4:x)", 4), 8: ("CS_CAM_AT_LIST(%h2:1:x, %h1:1:s, %h2:2:s, %h1:2:x)", 3, None, -1, "CS_CAM_POINT(%b4:1:e0, %b3:1:x, %h1:1:s, %w1:2:f, %h2:3:s, %h1:3:s, %h2:4:s, %h1:4:x)", 4), 1000: ("CS_DESTINATION(%h2:2:e1, %h1:2:s, %h2:3:s)", 4, None, None, None, None), 19: ("CS_TEXT_LIST(%w1:1:s)", 2, None, 0, "__SPECIAL(CS_TEXT_LIST)", 3), 45: ("CS_TRANSITION(%h2:2:e5, %h1:2:s, %h2:3:s)", 4, None, None, None, None), } begin_cutscene_entry = ("CS_BEGIN_CUTSCENE(%w1:0:s, %w1:1:s)", 2, None, None, None, None) unk_data_entry = ("CS_UNK_DATA_LIST(%w1:0:x, %w1:1:s)", 2, None, 0, "CS_UNK_DATA(%w1:1:x, %w1:2:x, %w1:3:x, %w1:4:x, %w1:5:x, %w1:6:x, %w1:7:x, %w1:8:x, %w1:9:x, %w1:10:x, %w1:11:x, %w1:12:x)", 12) linesep = "\n" indent = " " line_end = "," + linesep """ Since multiple command ids can map to the same command macro, to shorten the dictionary we introduce a function to allow these command ids to be grouped into tuples. """ def multi_key(key): for k in cutscene_command_macros.keys(): if type(k) is int: if k == key: return cutscene_command_macros[k] elif key in k: return cutscene_command_macros[k] """ Args """ def arg_count(cmd_def): return len([x for x in cmd_def.split("(")[1].split(")")[0].split(",")]) if "()" not in cmd_def else 0 def arg_part(arg, num): return arg.replace("%","").split(":")[num] def args_list(cmd_def): return [x.strip() for x in cmd_def.split("(")[1].split(")")[0].split(",")] """ Type conversion """ def get_byte(word, num): return struct.unpack(">bbbb", struct.pack(">I", word))[num] def get_byte_unsigned(word, num): return struct.unpack(">BBBB", struct.pack(">I", word))[num] def get_short(word, num): return struct.unpack(">hh", struct.pack(">I", word))[num] def get_short_unsigned(word, num): return struct.unpack(">HH", struct.pack(">I", word))[num] def get_word(word): return struct.unpack(">i", struct.pack(">I", word))[0] def get_word_unsigned(word): return struct.unpack(">I", struct.pack(">I", word))[0] def get_float(word): word_bytes = struct.pack(">i", word) v = struct.unpack(">f", word_bytes)[0] if v == 0: return v ndigits = -math.ceil(math.log10(abs(v))) while ndigits < 100: v_rounded = round(v, ndigits) if struct.pack(">f", v_rounded) == word_bytes: return v_rounded ndigits += 1 return v """ Formatting """ def pad(string, length): string = string.replace("0x","") if len(string) < length: return pad("0"+string, length) else: return string def format_arg(arg, words): word = words[int(arg_part(arg, 1))] # the word to look in arg_type = arg_part(arg, 0).strip()[0] # the type, b/h/w, tells you what part of the word to look in arg_type_num = int(arg_part(arg, 0).strip()[1]) # the type num, depends on type value = 0 unsigned_value = 0 pad_len = 0 if arg_type == "b": value = get_byte(word, 4 - arg_type_num) unsigned_value = get_byte_unsigned(word, 4 - arg_type_num) pad_len = 2 elif arg_type == "h": value = get_short(word, 2 - arg_type_num) unsigned_value = get_short_unsigned(word, 2 - arg_type_num) pad_len = 4 elif arg_type == "w": value = get_word(word) unsigned_value = get_word_unsigned(word) pad_len = 8 else: print("Something went wrong!") # TODO more debug info os._exit(1) format_type = arg_part(arg, 2).strip() # the format type, how to express the final value result = "" if "e" in format_type: enum_no = int(format_type[1]) if enum_no == 0: result = cscam_continue_stop[unsigned_value] elif enum_no == 1: result = cutscene_destinations[unsigned_value] elif enum_no == 2: result = ocarina_action_ids[unsigned_value] elif enum_no == 3: result = sequence_ids[unsigned_value - 1] elif enum_no == 4: result = cutscene_misc_types[unsigned_value] elif enum_no == 5: result = cutscene_transition_types[unsigned_value] elif enum_no == 6: result = player_cue_ids[unsigned_value] elif enum_no == 7: result = fade_out_seq_player[unsigned_value] elif format_type == "u": result = str(value) elif format_type == "s": result = str(value) elif format_type == "x": result = "0x" + pad(hex(unsigned_value), pad_len).upper() elif format_type == "x-1": assert unsigned_value > 0 result = "0x" + pad(hex(unsigned_value - 1), pad_len).upper() elif format_type == "f": result = f"CS_FLOAT(0x{unsigned_value:X}, {get_float(value)}f)" else: print("Something went wrong!") # TODO more debug info os._exit(1) return result def format_cmd(cmd, words): if "__SPECIAL" in cmd: # special handling for textbox list macros, as there are multiple dependent on particular values if "CS_TEXT_LIST" in cmd: if get_short_unsigned(words[1], 0) == 0xFFFF: cmd = "CS_TEXT_NONE(%h1:1:s, %h2:2:s)" elif get_short(words[2], 1) == 2: cmd = "CS_TEXT_OCARINA_ACTION(%h2:1:e2, %h1:1:s, %h2:2:s, %h2:3:x)" else: cmd = "CS_TEXT(%h2:1:x, %h1:1:s, %h2:2:s, %h1:2:x, %h2:3:x, %h1:3:x)" for arg in args_list(cmd): cmd = cmd.replace(arg, format_arg(arg, words)) return cmd """ Cutscene parser Note that this isn't protected against indexing errors since a cutscene should always end before the end of the file it's in. """ def disassemble_cutscene(cs_in): i = 0 total_entries = cs_in[i] i+=1 cutscene_end_frame = cs_in[i] i+=1 if (total_entries < 0 or cutscene_end_frame < 0): print("This cutscene would abort if played in-engine") if total_entries < 0: return "Could not disassemble cutscene: Number of commands is negative" macros = format_cmd(begin_cutscene_entry[0], [total_entries, cutscene_end_frame])+line_end for k in range(0,total_entries+1): cmd_type = cs_in[i] if (cmd_type == 0xFFFFFFFF): return macros + multi_key(-1)[0]+line_end entry = multi_key(cmd_type) if entry is None: entry = unk_data_entry cmd_macro = entry[0] n_words = entry[1] macros += format_cmd(cmd_macro, [cs_in[i+j] for j in range(0, n_words)])+line_end list_item = entry[4] if list_item is not None: # Not all macros have associated list item macros continue_stop = entry[2] cmd_entries = entry[3] n_words_list_item = entry[5] if continue_stop is not None: # Not all macros have a continue/stop flag i += n_words do_continue = True while do_continue: # "Integer Divide" by 4 to get the word it's in # Modulo operator to get the byte relative to the start of the word do_continue = True if get_byte(cs_in[i+(continue_stop // 4)], continue_stop % 4) == 0x00 else False macros += indent+format_cmd(list_item, [0, *[cs_in[i+j] for j in range(0, n_words_list_item)]])+line_end i += n_words_list_item elif cmd_entries is not None: # Not all macros have a defined number of entries if cmd_entries < 0: # Some have a fixed number of entries num_entries = -cmd_entries else: # Some specify their number of entries num_entries = cs_in[i+cmd_entries+1] i += n_words for n in range(0,num_entries): macros += indent+format_cmd(list_item, [0, *[cs_in[i+j] for j in range(0, n_words_list_item)]])+line_end i += n_words_list_item else: i += n_words print("Warning: cutscene reached maximum entries without encountering a CS_END command") return macros def hex_parse(s): return int(s, 16) def main(): parser = argparse.ArgumentParser(description="Disassembles cutscenes for OoT") parser.add_argument('address', help="VRAM or ROM address to disassemble at", type=hex_parse) args = parser.parse_args() file_result = None if args.address >= 0x80000000: file_result = filemap.GetFromVRam(args.address) else: file_result = filemap.GetFromRom(args.address) if file_result is None: print("Invalid address") os._exit(1) print(file_result) print() script_dir = os.path.dirname(os.path.realpath(__file__)) cs_data = None with open(script_dir + "/../extracted/gc-eu-mq-dbg/baserom/" + file_result.name, "rb") as ovl_file: ovl_file.seek(file_result.offset) cs_data = [i[0] for i in struct.iter_unpack(">I", bytearray(ovl_file.read()))] if cs_data is not None: print("static CutsceneData D_" + hex(args.address).replace("0x","").upper() + "[] = {\n" + indent+disassemble_cutscene(cs_data).replace(linesep,linesep+indent).rstrip()+"\n};") if __name__ == "__main__": main()