├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── RandomizerCore ├── ASM │ ├── Patches │ │ ├── cosmetics.asm │ │ ├── keysanity.asm │ │ ├── optional.asm │ │ ├── required.asm │ │ └── shop.asm │ ├── README.md │ └── assemble.py ├── Data │ ├── enemies.yml │ ├── items.yml │ ├── locations.yml │ └── logic.yml ├── LICENSE.txt ├── Paths │ ├── LICENSE.txt │ └── randomizer_paths.py ├── Randomizers │ ├── chests.py │ ├── conditions.py │ ├── crane_prizes.py │ ├── dampe.py │ ├── data.py │ ├── enemies.py │ ├── fishing.py │ ├── flags.py │ ├── golden_leaves.py │ ├── heart_pieces.py │ ├── instruments.py │ ├── item_drops.py │ ├── item_get.py │ ├── mad_batter.py │ ├── marin.py │ ├── miscellaneous.py │ ├── npcs.py │ ├── owls.py │ ├── player_start.py │ ├── rapids.py │ ├── rupees.py │ ├── seashell_mansion.py │ ├── shop.py │ ├── small_keys.py │ ├── tarin.py │ ├── trade_quest.py │ └── tunic_swap.py ├── Tools │ ├── bntx_editor │ │ ├── LICENSE.txt │ │ ├── bfres.py │ │ ├── bntx.py │ │ ├── bntx_editor.py │ │ ├── dds.py │ │ ├── formConv.py │ │ ├── globals.py │ │ ├── structs.py │ │ └── swizzle.py │ ├── bntx_tools.py │ ├── event_tools.py │ ├── exefs_editor │ │ ├── LICENSE.txt │ │ └── patcher.py │ ├── fixed_hash.py │ ├── leb.py │ ├── lvb.py │ └── oead_tools.py ├── mod_generator.py ├── randomizer_data.py ├── shuffler.py └── spoiler.py ├── RandomizerUI ├── Resources │ ├── about.txt │ ├── adjectives.txt │ ├── changes.txt │ ├── characters.txt │ ├── dark_theme.txt │ ├── icon.icns │ ├── icon.ico │ ├── issues.txt │ ├── light_theme.txt │ ├── randomizer.png │ ├── textures │ │ └── chest │ │ │ ├── TreasureBoxJunk.dds │ │ │ ├── TreasureBoxKey.dds │ │ │ └── TreasureBoxLifeUpgrade.dds │ └── tips.txt ├── UI │ ├── ui_form.py │ ├── ui_form.ui │ ├── ui_progress_form.py │ └── ui_progress_form.ui ├── main_window.py ├── progress_window.py ├── settings_manager.py └── update.py ├── build.bat ├── build.py ├── randomizer.py ├── requirements.txt ├── roadmap.txt ├── setup.py └── version.txt /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: workflow_dispatch 2 | 3 | jobs: 4 | prepare-build-files: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - run : echo "Replacing essential data with the ones from this repository instead of the default ones" 9 | - run : sed -i "s/EXTRA_TITLE_DATA\ =.*/EXTRA_TITLE_DATA\ = '- ${{ github.repository_owner }} - ${{ github.ref_name }}'/g" RandomizerCore/randomizer_data.py 10 | - run : sed -i "s/MAIN_BRANCH\ =.*/MAIN_BRANCH\ = '${{ github.event.repository.default_branch }}'/g" RandomizerCore/randomizer_data.py 11 | - run : sed -i "s/AUTHOR\ =.*/AUTHOR\ = '${{ github.repository_owner }}'/g" RandomizerCore/randomizer_data.py 12 | - uses: actions/upload-artifact@v4 13 | with: 14 | name: updated-version-data 15 | path: RandomizerCore/randomizer_data.py 16 | 17 | # Application build (For every OS) 18 | build: 19 | needs : prepare-build-files 20 | strategy: 21 | matrix: 22 | #os: [windows-latest, macos-latest, ubuntu-latest] 23 | os: [windows-latest, ubuntu-latest] 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - run: echo "Preparing '${{ github.event.ref }}' for release" 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.8' 31 | cache: 'pip' # caching pip dependencies 32 | # Force upgrading pip (Might be removed at some point) 33 | - run: pip install --upgrade pip 34 | - run: pip install -r requirements.txt 35 | - uses: actions/download-artifact@v4 36 | with: 37 | name: updated-version-data 38 | path: RandomizerCore 39 | - run: mkdir build 40 | - run: python setup.py build 41 | - run: python build.py 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: release-build-${{ matrix.os }} 45 | path: build/*.zip 46 | 47 | # dev-release: 48 | # needs: build 49 | # if: success() && (github.ref_name == github.event.repository.default_branch) 50 | # runs-on: ubuntu-latest 51 | # steps: 52 | # - uses: actions/download-artifact@v4 53 | # with: 54 | # path: build 55 | # pattern: release-build-* 56 | # merge-multiple: true 57 | # - uses: "marvinpinto/action-automatic-releases@latest" 58 | # with: 59 | # repo_token: "${{ secrets.GITHUB_TOKEN }}" 60 | # prerelease: true 61 | # automatic_release_tag: "testing" 62 | # title: "Release ${{ github.ref_name }}" 63 | # files: "build/*.zip" 64 | 65 | prod-release: 66 | needs: build 67 | if: success() 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/download-artifact@v4 71 | with: 72 | path: build 73 | pattern: release-build-* 74 | merge-multiple: true 75 | - uses: "marvinpinto/action-automatic-releases@latest" 76 | with: 77 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 78 | prerelease: false 79 | automatic_release_tag: "${{ github.ref_name }}" 80 | title: "Release ${{ github.ref_name }}" 81 | files: "build/*.zip" 82 | 83 | 84 | # Artifact Cleanup (to avoid too much storage) 85 | cleanup: 86 | # needs: [prepare-build-files, build, dev-release, prod-release] 87 | needs: [prepare-build-files, build, prod-release] 88 | if: ${{ always() || contains(needs.*.result, 'failure') }} 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: geekyeggo/delete-artifact@v4 92 | with: 93 | name: | 94 | updated-version-data 95 | release-build-* 96 | # As this is a cleanup job, if this fails due to non existing files, we don't really care. 97 | failOnError: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .qt_for_python 3 | /.idea 4 | /build 5 | /Models 6 | /output 7 | /RandomizerCore/Tools/text_tools.py 8 | /RandomizerUI/Resources/__Combined.bntx 9 | /RandomizerUI/Resources/textures/chest/bfres 10 | /Test 11 | tests.py 12 | settings.txt 13 | log.txt -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Owen_Splat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LAS-Randomizer 2 | A randomizer for The Legend of Zelda: Link's Awakening remake. 3 | 4 | You can download the randomizer here: https://github.com/Owen-Splat/LAS-Randomizer/releases/latest 5 | 6 | ## Information 7 | This randomizes all the items in the game so that every playthrough is unique. It also aims to add quality of life changes, such as skipping cutscenes, or adding options that make the game more open-world. 8 | 9 | **Logic**: 10 | - Basic: No glitches or advanced tricks, although 2 tile jumps are included. Recommended for beginners. 11 | - Advanced: Skews and bomb arrows will be added to logic. 12 | - Glitched: Adds some glitches to logic that will not result in being softlocked. 13 | - Hell: Adds more glitches to logic that can result in softlocks. 14 | - None: Throws all logic out the window. Seeds will likely be unbeatable. 15 | 16 | Please note that while most things are functional, there is a possibility of the logic resulting in softlocks. This will be especially true with the glitched logics. If this does happen, dying and selecting the Save + Quit option will respawn you back at Marin's house. 17 | 18 | ## How to play: 19 | Please refer to the setup wiki: https://github.com/Owen-Splat/LAS-Randomizer/wiki/Setup-Instuctions 20 | 21 | ## Discord Server 22 | Join the Discord server to talk about the randomizer, ask questions, or even set up races! 23 | The Discord also contains some more detailed information about the current state of the randomizer, including known issues and what is shuffled. 24 | 25 | https://discord.com/invite/rfBSCUfzj8 26 | 27 | ## Credits: 28 | - Glan: Created the original early builds of the randomizer found here: https://github.com/la-switch/LAS-Randomizer 29 | - j_im: Created the original tracker for the randomizer 30 | - Br00ty: Maintains and updates the tracker for newer randomizer versions 31 | - ProfessorLaw: Additional programming 32 | - theboy181: Blur removal patch 33 | 34 | ### Special Thanks: 35 | - To everyone who has reported bugs or given feedback and suggestions. This randomizer would not be where it is today without our community. 36 | 37 | ## Running from source: 38 | **NOTE**: This is for advanced users or those helping with the development of this randomizer. 39 | 40 | If you want to run from source, then you need to clone this repository and make sure you have Python 3.8+ installed 41 | 42 | Open the folder in a command prompt and install dependencies by running: 43 | `py -3.8 -m pip install -r requirements.txt` (on Windows) 44 | `python3 -m pip install -r requirements.txt` (on Mac) 45 | `python3 -m pip install $(cat requirements.txt) --user` (on Linux) 46 | 47 | Then run the randomizer with: 48 | `py -3.8 randomizer.py` (on Windows) 49 | `python3 randomizer.py` (on Mac) 50 | `python3 randomizer.py` (on Linux) 51 | 52 | If you are using a different version of Python, change the commands to include your version instead 53 | 54 | Once you have installed all the requirements, there is an included **build.bat** file. Run that (you can just enter `build` in the terminal) and it will automatically enter the commands to create a build. Once again, if you are using a different version of Python, you will need to edit this file to match your version 55 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/Patches/cosmetics.asm: -------------------------------------------------------------------------------- 1 | ; This contains patches that effect the visuals of the game 2 | 3 | 4 | ;* NPCs hold the proper item model before giving it to the player 5 | ; This is done by changing the itemID of the model to a new Items.gsheet entry with the proper model 6 | .offset 0x9fa0f0 ; bay-fisherman 7 | mov w4, #200 8 | .offset 0xa40374 ; syrup 9 | mov w3, #201 10 | .offset 0xa534a4 ; walrus 11 | mov w8, #202 12 | 13 | 14 | ;* Songs, tunics, and capacity upgrades show the correct item model 15 | ; This is done by making them go to the default itemID case 16 | ; Default case means it will use its own npcKey in Items.gsheet rather than a different item's npcKey 17 | .offset 0xd798c4 18 | b 0xd799f8 19 | .offset 0xd79814 20 | b 0xd799f8 21 | .offset 0xd79804 22 | b 0xd799f8 23 | 24 | 25 | ;* Removes the blur around the edge of the screen [theboy181] 26 | ; Changes the string "PFXTiltShiftParam" that is used in postprocess.bfsha shader file 27 | ; This new string can be anything that isn't found in the shader 28 | ;settings blur-removal 29 | .offset 0x16cbd73 30 | .string "NoTiltShift" 31 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/Patches/keysanity.asm: -------------------------------------------------------------------------------- 1 | ; This contains patches required for keysanity to work 2 | 3 | 4 | ; Allows using the item index to determine the dungeon the item goes to 5 | ; Replaces the variable holding the current level value with the item index 6 | ; This makes the dungeon items go to the dungeon that corresponds to the item index 7 | ; NOT WRITTEN YET BECAUSE IT NEEDS TO ONLY DO THIS IF YOU'RE NOT IN A DAMPE DUNGEON 8 | 9 | 10 | ;* Allows dungeon items to be obtained outside of dungeons 11 | ; For dungeon items, compare the item count instead of current level/item index 12 | ; This lets it work outside of dungeons as well as supporting item indexes of -1 13 | ;settings keys 14 | .offset 0x8d0cf4 ; SmallKey 15 | cmp w8, #-1 16 | .offset 0x8d0cf8 17 | b.eq 0x8d0d00 18 | mov w8, #8 19 | .offset 0x8d0e58 ; NightmareKey 20 | cmp w8, #-1 21 | .offset 0x8d0e5c 22 | b.eq 0x8d0e64 23 | mov w8, #8 24 | 25 | ;settings keys+mcb 26 | .offset 0x8d0e04 ; Compass 27 | cmp w8, #-1 28 | .offset 0x8d0e08 29 | b.eq 0x8d0e10 30 | mov w8, #8 31 | .offset 0x8d1278 ; DungeonMap 32 | cmp w8, #-1 33 | .offset 0x8d127c 34 | b.eq 0x8d1284 35 | mov w8, #8 36 | .offset 0x8d1478 ; StoneBeak 37 | cmp w8, #-1 38 | .offset 0x8d147c 39 | b.eq 0x8d1484 40 | mov w8, #8 41 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/Patches/optional.asm: -------------------------------------------------------------------------------- 1 | ; This contains the patches required to make miscellaneous, optional settings work 2 | 3 | 4 | ;* Read the egg path without Lens 5 | ; This is done by making Inventory.HasItem(44) always return True 6 | ;settings free-book 7 | .offset 0x7e3004 8 | mov w0, #1 9 | 10 | 11 | ;* Randomize the EnemyZolGreen inside chests 12 | ;settings randomize-enemies 13 | .offset 0xca92c0 14 | mov w9, x0 ;data CHEST_ENEMY 15 | 16 | 17 | ;* Make all forms of damage kill Link in 1 hit 18 | ;settings OHKO 19 | .offset 0xd4c754 ; normal damage 20 | sub w22, w8, #80 21 | .offset 0xdb1f74 ; fall/drown damage 22 | sub w8, w21, #80 23 | .offset 0xd7c8c8 ; # trap damage 24 | sub w20, w8, #80 25 | .offset 0xd96950 ; blaino damage 26 | sub w8, w23, #80 27 | 28 | 29 | ;* Beam slash with base sword 30 | ;settings lv1-beam 31 | .offset 0xde1ba8 32 | ldrb w9, [x8, #0xa8] 33 | 34 | 35 | ;* Rapid-fire Magic Rod 36 | ; This is done by changing the Magic Rod projectile instance limit from 3 to 16 37 | ;settings nice-rod 38 | .offset 0xd51698 39 | cmp x19, #16 40 | 41 | 42 | ;* Rapid-fire Bombs 43 | ; This is done by changing a boolean value to always be True 44 | ;settings nice-bombs 45 | .offset 0xd52958 46 | mov w8, #1 47 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/Patches/required.asm: -------------------------------------------------------------------------------- 1 | ; This contains the patches that are absolutely required for the randomizer to work 2 | 3 | 4 | ;* Open Color Dungeon with companions 5 | ; This is done by changing "companion == 0" to "companion != 5" 6 | ; Since companion value 5 is not a thing, the check always returns True 7 | .offset 0xc868d4 8 | ccmp w9, #0, #5, ne 9 | 10 | 11 | ;* Make EnemySoldierIronBall ignore GoldenLeaf[4] 12 | ; This is done by instead running the code that checks for the Actor Switch flag 13 | ; For the randomizer, the flag is set True when the item drops after defeating the soldier 14 | .offset 0x6a62f8 15 | cbz w0, 0x6a6340 16 | 17 | 18 | ;* Rewrite Inventory::RemoveItem(0) to remove Bottle[1] 19 | ; This is so that we can actively add/remove it from inventory to control if it shows in the FishingPond 20 | .offset 0x7e1f6c 21 | adrp x8, 0x1cc1368 ; inventory offset, the assembler handles converting to page offset 22 | ldr x8, [x8, #0x368] 23 | ldr w9, [x8, #0xa8] 24 | and w9, w9, 0xFFFFBFFF 25 | str w9, [x8, #0xa8] 26 | b 0x7e1dd0 27 | 28 | 29 | ;* Rewrite FlowControl::CompareInt event to check if the values are equal 30 | ; To match FlowControl::CompareString, it returns 0 if they are equal, 1 if not 31 | ; This allows us to check the index of items through the EventFlow system 32 | ; The main purpose of this will be for Keysanity to know which dungeon text to display 33 | ; This is also used to set a flag for the Fishing Bottle 34 | .offset 0x8049d8 35 | mov w8, #1 36 | 37 | 38 | ;* Make Bombs/Arrows/Powder give 3 for a single drop 39 | .offset 0x88f674 40 | mov w4, #3 41 | .offset 0x895674 42 | mov w4, #3 43 | .offset 0x16fae60 44 | .short #3 ; ItemMagicPowder count is stored in the data section instead of a local variable 45 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/Patches/shop.asm: -------------------------------------------------------------------------------- 1 | ; This contains the patches related to the shop 2 | 3 | 4 | ;* Always allow stealing 5 | ; Ignores sword check and checks shopkeeper direction 6 | ;settings stealing 7 | .offset 0xa4a8f0 8 | b 0xa4a910 9 | 10 | 11 | ;* Never allow stealing 12 | ; Ignores sword check and prevents the player from stealing 13 | ;settings !stealing 14 | .offset 0xa4a8f0 15 | b 0xa4a8f4 16 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/README.md: -------------------------------------------------------------------------------- 1 | ### Comments 2 | lines starting with "; " are dev comments 3 | 4 | lines starting with ";* " are patch titles that will show in the .pchtxt files 5 | 6 | ### Settings 7 | to access a randomizer setting for a patch, the line must look like ";settings SETTING_NAME" 8 | 9 | ".settings !SETTING_NAME" also works for patches that require a setting to be off 10 | 11 | ### Data 12 | asm that requires randomizer data needs the instruction to end with ";data VARIABLE_NAME" 13 | 14 | it will replace the last register on that line 15 | 16 | this data is compiled in the assemble.py preSetup function 17 | 18 | right now, this is just for the randomized chest enemy 19 | 20 | ### Patches 21 | patches are finished by blank lines or lines starting with ".offset" 22 | 23 | settings based patches need a blank line afterwards in order for the condition to be reset 24 | 25 | just reference the existing patches if you're confused by the format 26 | 27 | ### Credits 28 | if you add a patch, please credit with [ username ] 29 | 30 | patches without explicit credit can be used by anyone else, no credit needed 31 | -------------------------------------------------------------------------------- /RandomizerCore/ASM/assemble.py: -------------------------------------------------------------------------------- 1 | from RandomizerCore.Tools.exefs_editor.patcher import Patcher 2 | from RandomizerCore.Paths.randomizer_paths import ASM_PATH, IS_RUNNING_FROM_SOURCE 3 | import os, random 4 | 5 | 6 | def createRandomizerPatches(rand_state: tuple, settings: dict): 7 | asm_data = preSetup(rand_state, settings) 8 | patcher = Patcher() 9 | 10 | asm_files = [f for f in os.listdir(ASM_PATH) if f.endswith('.asm')] 11 | for asm in asm_files: 12 | patches = readASM(asm, asm_data, settings) 13 | for patch in patches: 14 | address, instruction, comment = patch 15 | if instruction.startswith('.string'): 16 | instruction = instruction.split('.string ')[1] 17 | instruction = instruction.split('; ')[0] 18 | patcher.replaceString(address, instruction, comment) 19 | elif instruction.startswith('.short'): 20 | instruction = instruction.split('.short ')[1] 21 | instruction = instruction.split('; ')[0] 22 | instruction = instruction.replace('#', '') 23 | patcher.replaceShort(address, int(instruction), comment) 24 | else: 25 | patcher.addPatch(address, instruction, comment) 26 | 27 | return patcher 28 | 29 | 30 | def readASM(asm, asm_data, settings): 31 | with open(f'{ASM_PATH}/{asm}', 'r') as f: 32 | asm_lines = f.read().splitlines() 33 | 34 | patches = [] 35 | offset = 0 36 | instruction = '' 37 | patch_info = '' 38 | condition_met = True 39 | 40 | for line in asm_lines: 41 | line = line.strip() 42 | 43 | # store pchtxt patch titles, skip over comments 44 | if line.startswith(';*'): 45 | if len(patch_info) > 0: 46 | patch_info += '\n' 47 | patch_info += line.replace(';*', '//') 48 | continue 49 | elif line.startswith('; '): 50 | continue 51 | 52 | # parse condition 53 | if line.startswith(';settings'): 54 | condition = line.split(' ')[1] 55 | state = True 56 | if condition.startswith('!'): 57 | state = False 58 | condition = condition.split('!')[1] 59 | if condition not in settings: 60 | condition_met = False 61 | else: 62 | condition_met = True if settings[condition] == state else False 63 | continue 64 | 65 | # add the patch if the line is blank, reset data and skip 66 | # reset condition & comment block, skip 67 | if len(line) == 0: 68 | if offset > 0 and len(instruction) > 0 and condition_met: 69 | patches.append((offset, instruction, patch_info)) 70 | condition_met = True 71 | instruction = '' 72 | patch_info = '' 73 | offset = 0 74 | continue 75 | 76 | # skip lines if the condition is not met (until blank line which resets the condition) 77 | if not condition_met: 78 | continue 79 | 80 | # replace data in line 81 | if ';data' in line: 82 | line_data = line.split(';data ') 83 | needed_data = asm_data[line_data[1]] 84 | line = line_data[0].strip() 85 | register = line[-3:].strip() 86 | line = line.replace(register, needed_data) 87 | 88 | # strip any mid-line comments 89 | if ';' in line: 90 | line = line.split(';')[0] 91 | line = line.strip() 92 | 93 | # add patch if there's still any, reset data and store new offset 94 | if line.startswith('.offset'): 95 | if offset > 0 and len(instruction) > 0: 96 | patches.append((offset, instruction, patch_info)) 97 | instruction = '' 98 | patch_info = '' 99 | offset = int(line.split(' ')[1][2:], 16) 100 | continue 101 | 102 | # handle branch offsets 103 | if line.startswith('b'): 104 | line_data = line.split(' ') 105 | branch_offset = int(line_data[1][2:], 16) 106 | diff = branch_offset - offset 107 | symbol = '+' 108 | if diff < 0: 109 | symbol = '' # if the diff is negative, it already has a - in front 110 | line = f"{line_data[0]} {symbol}{hex(diff)}" 111 | 112 | # handle adrp offsets 113 | # to make it easier to write patches, we can just write the absolute offset instead doing the page offset math 114 | if line.startswith('adrp'): 115 | line_data = line.split(', ') 116 | data_offset = line_data[1] 117 | line_data = line_data[0] 118 | data_offset = data_offset.replace(data_offset[-3:], '000', -1) 119 | page_offset = hex(offset) 120 | page_offset = page_offset.replace(page_offset[-3:], '000', -1) 121 | diff = int(data_offset, 16) - int(page_offset, 16) 122 | line = f"{line_data}, {hex(diff)}" 123 | 124 | # strip line of any remaining whitespace, add multi-line asm separator 125 | line = line.strip() 126 | instruction += line + '; ' 127 | 128 | if offset > 0 and len(instruction) > 0: 129 | patches.append((offset, instruction, patch_info)) 130 | 131 | if IS_RUNNING_FROM_SOURCE: 132 | for patch in patches: 133 | if len(patch[2]) > 0: 134 | print('\n' + patch[2]) 135 | print(hex(patch[0])) 136 | print(patch[1]) 137 | 138 | return patches 139 | 140 | 141 | def preSetup(rand_state, settings): 142 | random.setstate(rand_state) 143 | asm_data = {} 144 | 145 | # store the actor ID of the randomized chest enemy 146 | if settings['randomize-enemies']: 147 | from RandomizerCore.randomizer_data import ENEMY_DATA 148 | asm_data['CHEST_ENEMY'] = f"#{random.choice(ENEMY_DATA['Chest_Enemies'])}" 149 | 150 | # since the patches are written last, we can just change the stealing setting to a boolean 151 | if settings['stealing'] == 'always': 152 | settings['stealing'] = True 153 | elif settings['stealing'] == 'never': 154 | settings['stealing'] = False 155 | 156 | return asm_data 157 | -------------------------------------------------------------------------------- /RandomizerCore/Data/locations.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Chest_Locations: 3 | - beach-chest 4 | - taltal-entrance-chest 5 | - taltal-east-left-chest 6 | - dream-shrine-right 7 | - armos-cave 8 | - goponga-cave-left 9 | - goponga-cave-right 10 | - ukuku-cave-west-chest 11 | - ukuku-cave-east-chest 12 | - kanalet-south-cave 13 | - swamp-chest 14 | - taltal-left-ascent-cave 15 | - taltal-ledge-chest 16 | - taltal-5-chest-puzzle 17 | - taltal-west-chest 18 | - villa-cave 19 | - woods-crossing-cave-chest 20 | - woods-north-cave-chest 21 | - woods-south-chest 22 | - woods-north-chest 23 | 24 | Fishing_Rewards: 25 | - fishing-50 26 | - fishing-orange 27 | - fishing-loose 28 | - fishing-100 29 | - fishing-150 30 | - fishing-cheep-cheep 31 | - fishing-ol-baron 32 | 33 | Rapids_Rewards: 34 | - rapids-race-45 35 | - rapids-race-35 36 | - rapids-race-30 37 | - rapids-middle-island 38 | - rapids-west-island 39 | 40 | Dampe_Rewards: 41 | - dampe-page-1 42 | - dampe-heart-challenge 43 | - dampe-page-2 44 | - dampe-bottle-challenge 45 | - dampe-final 46 | 47 | Free_Gifts: 48 | - walrus 49 | - ghost-reward 50 | - marin 51 | - manbo 52 | - mamu 53 | - mad-batter-bay 54 | - mad-batter-taltal 55 | - mad-batter-woods 56 | - D0-fairy-1 57 | - D0-fairy-2 58 | - invisible-zora 59 | - goriya-trader 60 | 61 | Trade_Gifts: 62 | - mamasha 63 | - ciao-ciao 64 | - sale 65 | - kiki 66 | - tarin-ukuku 67 | - chef-bear 68 | - papahl 69 | - christine-trade 70 | - christine-grateful 71 | - mr-write 72 | - grandma-yahoo 73 | - bay-fisherman 74 | - mermaid-martha 75 | - mermaid-cave 76 | 77 | Boss_Locations: 78 | - lanmola 79 | - armos-knight 80 | - D1-moldorm 81 | - D2-genie 82 | - D3-slime-eye 83 | - D4-angler 84 | - D5-slime-eel 85 | - D5-master-stalfos 86 | - D6-facade 87 | - D7-eagle 88 | - D8-hothead 89 | 90 | Misc_Items: 91 | - washed-up 92 | - dream-shrine-left 93 | - taltal-rooster-cave 94 | - woods-loose 95 | - pothole-final 96 | 97 | Mansion: 98 | - 5-seashell-reward 99 | - 15-seashell-reward 100 | - 30-seashell-reward 101 | - 40-seashell-reward 102 | - 50-seashell-reward 103 | 104 | # Shop_Items: 105 | # - shop-slot3-1st 106 | # - shop-slot3-2nd 107 | # - shop-slot6 108 | 109 | # Trendy_Rewards: 110 | # - trendy-prize-1 111 | # - trendy-prize-2 112 | # - trendy-prize-3 113 | # - trendy-prize-4 114 | # - trendy-prize-5 115 | # - trendy-prize-6 116 | # - trendy-prize-final 117 | 118 | Heart_Pieces: 119 | - animal-village-northwest 120 | - animal-village-cave 121 | - taltal-entrance-blocks 122 | - north-wasteland 123 | - desert-cave 124 | - graveyard-cave 125 | - mabe-well 126 | - ukuku-cave-west-loose 127 | - ukuku-cave-east-loose 128 | # - bay-passage-sunken 129 | # - river-crossing-cave 130 | - rapids-ascent-cave 131 | # - kanalet-moat-south 132 | # - south-bay-sunken 133 | - taltal-crossing-cave 134 | # - taltal-east-drop 135 | - taltal-west-escape 136 | - above-turtle-rock 137 | - pothole-north 138 | - woods-crossing-cave-loose 139 | - woods-north-cave-loose 140 | - diamond-island 141 | 142 | Golden_Leaves: 143 | - kanalet-crow 144 | - kanalet-mad-bomber 145 | - kanalet-kill-room 146 | - kanalet-bombed-guard 147 | - kanalet-final-guard 148 | 149 | Dungeon_Owl_Statues: 150 | - D1-owl-statue-long-hallway 151 | - D1-owl-statue-spinies 152 | - D1-owl-statue-3-of-a-kind 153 | - D2-owl-statue-first-switch 154 | - D2-owl-statue-push-puzzle 155 | - D2-owl-statue-past-hinox 156 | - D3-owl-statue-basement-north 157 | - D3-owl-statue-arrow 158 | - D3-owl-statue-northwest 159 | - D4-owl-statue 160 | - D5-owl-statue-triple-stalfos 161 | - D5-owl-statue-before-slime-eel 162 | - D6-owl-statue-ledge 163 | - D6-owl-statue-southeast 164 | - D6-owl-statue-canal 165 | - D7-owl-statue-ball 166 | - D7-owl-statue-kirbys 167 | - D7-owl-statue-hookshot-chest 168 | - D8-owl-statue-above-smasher 169 | - D8-owl-statue-below-gibdos 170 | - D8-owl-statue-eye-statue 171 | - D0-owl-statue-nine-switches 172 | - D0-owl-statue-first-switches 173 | - D0-owl-statue-before-mini-boss 174 | 175 | Overworld_Owl_Statues: 176 | - owl-statue-below-D8 177 | - owl-statue-moblin-cave 178 | - owl-statue-pothole 179 | - owl-statue-above-cave 180 | - owl-statue-south-bay 181 | - owl-statue-desert 182 | - owl-statue-maze 183 | - owl-statue-rapids 184 | - owl-statue-taltal-east 185 | 186 | Blue_Rupees: 187 | - D0-rupee-1 188 | - D0-rupee-2 189 | - D0-rupee-3 190 | - D0-rupee-4 191 | - D0-rupee-5 192 | - D0-rupee-6 193 | - D0-rupee-7 194 | - D0-rupee-8 195 | - D0-rupee-9 196 | - D0-rupee-10 197 | - D0-rupee-11 198 | - D0-rupee-12 199 | - D0-rupee-13 200 | - D0-rupee-14 201 | - D0-rupee-15 202 | - D0-rupee-16 203 | - D0-rupee-17 204 | - D0-rupee-18 205 | - D0-rupee-19 206 | - D0-rupee-20 207 | - D0-rupee-21 208 | - D0-rupee-22 209 | - D0-rupee-23 210 | - D0-rupee-24 211 | - D0-rupee-25 212 | - D0-rupee-26 213 | - D0-rupee-27 214 | - D0-rupee-28 215 | ... -------------------------------------------------------------------------------- /RandomizerCore/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 la-switch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RandomizerCore/Paths/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 LagoLunatic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RandomizerCore/Paths/randomizer_paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import appdirs 4 | import platform 5 | 6 | # check if user is running a precompiled binary 7 | if getattr(sys, "frozen", False): 8 | IS_RUNNING_FROM_SOURCE = False 9 | ROOT_PATH = os.path.dirname(sys.executable) 10 | ASM_PATH = os.path.join(ROOT_PATH, 'lib/RandomizerCore/ASM/Patches') 11 | DATA_PATH = os.path.join(ROOT_PATH, 'Data') 12 | RESOURCE_PATH = os.path.join(ROOT_PATH, 'Resources') 13 | if platform.system() == 'Darwin': 14 | userdata_path = appdirs.user_data_dir('randomizer', 'LAS Randomizer') 15 | if not os.path.isdir(userdata_path): 16 | os.mkdir(userdata_path) 17 | SETTINGS_PATH = os.path.join(userdata_path, 'settings.txt') 18 | LOGS_PATH = os.path.join(userdata_path, 'log.txt') 19 | else: 20 | SETTINGS_PATH = os.path.join('.', 'settings.txt') 21 | LOGS_PATH = os.path.join('.', 'log.txt') 22 | else: 23 | IS_RUNNING_FROM_SOURCE = True 24 | ROOT_PATH = os.path.dirname(sys.argv[0]) 25 | ASM_PATH = os.path.join(ROOT_PATH, 'RandomizerCore/ASM/Patches') 26 | DATA_PATH = os.path.join(ROOT_PATH, 'RandomizerCore/Data') 27 | RESOURCE_PATH = os.path.join(ROOT_PATH, 'RandomizerUI/Resources') 28 | SETTINGS_PATH = os.path.join(ROOT_PATH, 'settings.txt') 29 | LOGS_PATH = os.path.join(ROOT_PATH, 'log.txt') 30 | 31 | LOGIC_PATH = os.path.join(DATA_PATH, 'logic.yml') 32 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/conditions.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.oead_tools as oead_tools 2 | from RandomizerCore.Randomizers import data 3 | 4 | 5 | 6 | def makeConditions(sheet, placements): 7 | """Create new condition sets for the seashell sensor to work with Dampe, Rapids guy, Fishing Guy, and Seashell mansion""" 8 | 9 | dampe_condition = oead_tools.createCondition('DampeShellsComplete', [(9, 'true')]) 10 | dampe_locations = ['dampe-page-1', 'dampe-heart-challenge', 'dampe-page-2', 'dampe-bottle-challenge', 'dampe-final'] 11 | for location in dampe_locations: 12 | if placements[location] == 'seashell': 13 | dampe_condition['conditions'].append({'category': 2, 'parameter': f"Seashell:{placements['indexes'][location]}"}) 14 | sheet['values'].append(dampe_condition) 15 | 16 | rapids_condition = oead_tools.createCondition('RapidsShellsComplete', [(9, 'true')]) 17 | rapids_locations = ['rapids-race-30', 'rapids-race-35', 'rapids-race-45'] 18 | for location in rapids_locations: 19 | if placements[location] == 'seashell': 20 | rapids_condition['conditions'].append({'category': 2, 'parameter': f"Seashell:{placements['indexes'][location]}"}) 21 | sheet['values'].append(rapids_condition) 22 | 23 | fishing_condition = oead_tools.createCondition('FishingShellsComplete', [(9, 'true')]) 24 | fishing_locations = ['fishing-orange', 'fishing-cheep-cheep', 'fishing-ol-baron', 'fishing-loose', 'fishing-50', 'fishing-100', 'fishing-150'] 25 | for location in fishing_locations: 26 | if placements[location] == 'seashell': 27 | fishing_condition['conditions'].append({'category': 2, 'parameter': f"Seashell:{placements['indexes'][location]}"}) 28 | sheet['values'].append(fishing_condition) 29 | 30 | mansion_condition = oead_tools.createCondition('MansionShellsComplete', [(9, 'true')]) 31 | mansion_locations = ['5-seashell-reward', '15-seashell-reward', '30-seashell-reward', '40-seashell-reward', '50-seashell-reward'] 32 | for location in mansion_locations: 33 | if placements[location] == 'seashell': 34 | mansion_condition['conditions'].append({'category': 2, 'parameter': f"Seashell:{placements['indexes'][location]}"}) 35 | sheet['values'].append(mansion_condition) 36 | 37 | 38 | 39 | def editConditions(condition, settings): 40 | """Makes needed changes to conditions, such as making Marin staying in Mabe and the shop not sell shields until you find one""" 41 | 42 | # Make sure Marin always stays in the village even if you trade for the pineapple 43 | if condition['symbol'] == 'MarinVillageStay': 44 | condition['conditions'].pop(1) 45 | return 46 | 47 | # Make the animals in Animal village not be in the ring, which they would because of WalrusAwaked getting set 48 | if condition['symbol'] == 'AnimalPop': 49 | condition['conditions'][0] = {'category': 9, 'parameter': 'false'} 50 | return 51 | 52 | # Make Grandma Yahoo's broom invisible until you give her the broom 53 | if condition['symbol'] == 'BroomInvisible': 54 | condition['conditions'].pop(0) 55 | return 56 | 57 | if condition['symbol'] == 'BowWowMissionStart': # Remove BoyA condition 58 | condition['conditions'].pop(0) 59 | return 60 | 61 | if condition['symbol'] == 'BowWowMissionEnd': # Remove BoyA condition 62 | condition['conditions'].pop(0) 63 | return 64 | 65 | # Make the shop not sell shields until you find one 66 | if condition['symbol'] == 'ShopShieldCondition': 67 | condition['conditions'][0] = {'category': 1, 'parameter': data.SHIELD_FOUND_FLAG} 68 | return 69 | 70 | # Make the shop not sell bombs until you find some (flag automatically set with unlocked-bombs on) 71 | # Condition stays as ConchHorn if neither unlocked-bombs or shuffled-bombs is on 72 | if condition['symbol'] == 'ShopBombCondition' and (settings['unlocked-bombs'] or settings['shuffle-bombs']): 73 | condition['conditions'][0] = {'category': 1, 'parameter': data.BOMBS_FOUND_FLAG} 74 | return 75 | 76 | # # Edit the shop conditions for the shovel, bow, and heart 77 | # if condition['symbol'] == 'ShopShovelCondition': 78 | # condition['conditions'].pop(0) 79 | # condition['conditions'][0] = {'category': 1, 'parameter': '!ShopShovelGet'} 80 | # return 81 | 82 | # if condition['symbol'] == 'ShopBowCondition': 83 | # condition['conditions'][0] = {'category': 1, 'parameter': 'ShopShovelGet'} 84 | # condition['conditions'][1] = {'category': 1, 'parameter': '!ShopBowGet'} 85 | # return 86 | 87 | # # if condition['symbol'] == 'ShopArrowCondition': 88 | # # condition['conditions'][0]['category'] = 2 # change Bow check to category 2 instead of the weird category 11 89 | # # condition['conditions'].append({'category': 1, 'parameter': 'ShopBowGet'}) 90 | # # return 91 | 92 | # if condition['symbol'] == 'ShopHeartPieceCondition': 93 | # condition['conditions'][0] = {'category': 1, 'parameter': '!ShopHeartGet'} 94 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/dampe.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get, data 3 | 4 | 5 | 6 | def makeDatasheetChanges(sheet, reward_num, item_key): 7 | """Edits the Dampe rewards datasheets to give fake items 8 | 9 | The fake items set flags which are then used to do the real work after""" 10 | 11 | sheet['values'][reward_num]['mRewardItem'] = item_key 12 | sheet['values'][reward_num]['mRewardItemIndex'] = 0 13 | sheet['values'][reward_num]['mRewardItemEventEntry'] = item_key 14 | 15 | 16 | 17 | def makeEventChanges(flowchart, item_defs, placements): 18 | """Make Dampe perform inventory and flag checks before and after the reward event""" 19 | 20 | remove_final = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 21 | {'symbol': 'DampeFinal', 'value': False}, 'Event42') 22 | give_final = item_get.insertDampeItemGet(flowchart, 23 | item_defs[placements['dampe-final']]['item-key'], 24 | placements['indexes']['dampe-final'] if 'dampe-final' in placements['indexes'] else -1, 25 | remove_final) 26 | check_final = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 27 | {'symbol': 'DampeFinal'}, {0: 'Event42', 1: give_final}) 28 | 29 | remove_bottle = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 30 | {'symbol': 'DampeBottle', 'value': False}, check_final) 31 | give_bottle = item_get.insertDampeItemGet(flowchart, 32 | item_defs[placements['dampe-bottle-challenge']]['item-key'], 33 | placements['indexes']['dampe-bottle-challenge'] if 'dampe-bottle-challenge' in placements['indexes'] else -1, 34 | remove_bottle) 35 | check_bottle = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 36 | {'symbol': 'DampeBottle'}, {0: check_final, 1: give_bottle}) 37 | 38 | remove_page2 = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 39 | {'symbol': 'Dampe2', 'value': False}, check_bottle) 40 | give_page2 = item_get.insertDampeItemGet(flowchart, 41 | item_defs[placements['dampe-page-2']]['item-key'], 42 | placements['indexes']['dampe-page-2'] if 'dampe-page-2' in placements['indexes'] else -1, 43 | remove_page2) 44 | check_page2 = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 45 | {'symbol': 'Dampe2'}, {0: check_bottle, 1: give_page2}) 46 | 47 | remove_heart = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 48 | {'symbol': 'DampeHeart', 'value': False}, check_page2) 49 | give_heart = item_get.insertDampeItemGet(flowchart, 50 | item_defs[placements['dampe-heart-challenge']]['item-key'], 51 | placements['indexes']['dampe-heart-challenge'] if 'dampe-heart-challenge' in placements['indexes'] else -1, 52 | remove_heart) 53 | check_heart = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 54 | {'symbol': 'DampeHeart'}, {0: check_page2, 1: give_heart}) 55 | 56 | remove_page1 = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 57 | {'symbol': 'Dampe1', 'value': False}, check_heart) 58 | give_page1 = item_get.insertDampeItemGet(flowchart, 59 | item_defs[placements['dampe-page-1']]['item-key'], 60 | placements['indexes']['dampe-page-1'] if 'dampe-page-1' in placements['indexes'] else -1, 61 | remove_page1) 62 | check_page1 = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 63 | {'symbol': 'Dampe1'}, {0: check_heart, 1: give_page1}) 64 | 65 | event_tools.insertEventAfter(flowchart, 'Event39', check_page1) 66 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/enemies.py: -------------------------------------------------------------------------------- 1 | import random 2 | from RandomizerCore.randomizer_data import ENEMY_DATA 3 | 4 | 5 | 6 | def shuffleEnemyActors(room_data, folder: str, file: str, enemy_ids: dict, enemy_settings: dict, rand_state: tuple): 7 | """""" 8 | 9 | random.setstate(rand_state) 10 | edited_room = False 11 | 12 | excluded_actors = [] 13 | if file[:-4] in list(ENEMY_DATA['Excluded_Actors'].keys()): 14 | excluded_actors = ENEMY_DATA['Excluded_Actors'][file[:-4]] 15 | 16 | restr = list(enemy_ids['restr']) 17 | 18 | # we want to exclude certain enemies from being in 2D rooms 19 | # some can block your path, others might just be annoying 20 | if file[:-4] in ENEMY_DATA['2D_Rooms']: 21 | no_2D = (0x1E, 0x30, 0x37, 0x38, 0x3F, 0x41, 0x4A, 0x4D) 22 | restr.extend(no_2D) 23 | 24 | total_ids = ( 25 | *enemy_ids['land'], 26 | *enemy_ids['air'], 27 | *enemy_ids['water'], 28 | *enemy_ids['water2D'], 29 | *enemy_ids['water_shallow'], 30 | *enemy_ids['tree'], 31 | *enemy_ids['hole'] 32 | ) 33 | fly_bombs = (0x26, 0x3E, 0x48) # vires, zirros, bone-putters 34 | blocking = (0x8, 0x9, 0x13, 0x14, 0x2E, 0x2F, 0x30, 0x35, 0x36, 0x41, 0x4D) # shield/spear, mimics, color dungeon orbs 35 | 36 | # iterate through each actor 37 | for e, act in enumerate(room_data.actors): 38 | if act.type not in total_ids or e in excluded_actors: # check for valid enemy actors 39 | continue 40 | 41 | if enemy_settings['types']: 42 | enemy_type = [v for k,v in ENEMY_DATA['Actors'].items() if v['id'] == act.type][0]['type'] 43 | new_enemy = -1 44 | 45 | if act.type in restr: 46 | restr.remove(act.type) 47 | 48 | # keep shuffling each actor until it is a valid enemy 49 | while new_enemy in restr: 50 | if enemy_type == 'land': 51 | new_enemy = random.choice(enemy_ids['land']) 52 | elif enemy_type == 'air': 53 | if folder == 'Field': 54 | new_enemy = random.choice(enemy_ids['no_vire']) # remove vires from overworld 55 | else: 56 | new_enemy = random.choice(enemy_ids['air']) 57 | elif enemy_type == 'water': 58 | waters = (*enemy_ids['water'], *enemy_ids['water_shallow']) 59 | new_enemy = random.choice(waters) 60 | elif enemy_type == 'water2D': 61 | new_enemy = random.choice(enemy_ids['water2D']) 62 | elif enemy_type == 'water-shallow': 63 | new_enemy = random.choice(enemy_ids['water_shallow']) 64 | elif enemy_type == 'tree': 65 | new_enemy = random.choice(enemy_ids['tree']) 66 | elif enemy_type == 'hole': 67 | new_enemy = random.choice(enemy_ids['hole']) 68 | 69 | ### restrict enemy groups to one per room 70 | if new_enemy in fly_bombs: 71 | restr.extend(fly_bombs) 72 | elif new_enemy in blocking: 73 | restr.extend(blocking) 74 | 75 | # change the enemy data into the new enemy 76 | if act.type != new_enemy: 77 | act.type = new_enemy 78 | try: 79 | params = [v for k,v in ENEMY_DATA['Actors'].items() if v['id'] == act.type][0]['parameters'] 80 | for i in range(8): 81 | try: 82 | if isinstance(params[i], list): 83 | param = random.choice(params[i]) 84 | else: 85 | param = params[i] 86 | if isinstance(param, str): 87 | param = bytes(param, 'utf-8') 88 | act.parameters[i] = param 89 | except IndexError: 90 | act.parameters[i] = b'' 91 | except KeyError: 92 | act.parameters = [b'', b'', b'', b'', b'', b'', b'', b''] 93 | 94 | act.relationships.e = int([v for k,v in ENEMY_DATA['Actors'].items() if v['id'] == act.type][0]['enemy']) 95 | 96 | if act.type == 0x4A: # StretchyGhosts - only includes one color in pool so it will be 1/3 likely now 97 | act.type = random.choice((0x4A, 0x4B, 0x4C)) # decide color 98 | elif act.type == 0x4D: # ColorDungeon Orbs - same thing as above 99 | act.type = random.choice((0x4D, 0x4E, 0x4F)) 100 | 101 | act.rotY = 0 # change each enemy to be facing the screen, some will stay sideways if we don't 102 | 103 | # make sure actor is default size so that anything that's normally big won't cause the new enemy to be big 104 | act.scaleX = 1.0 105 | act.scaleY = 1.0 106 | act.scaleZ = 1.0 107 | edited_room = True 108 | 109 | if enemy_settings['sizes']: 110 | new_scale = random.uniform(0.5, 1.5) 111 | act.scaleX = new_scale 112 | act.scaleY = new_scale 113 | act.scaleZ = new_scale 114 | edited_room = True 115 | 116 | return random.getstate(), edited_room 117 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/fishing.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def makeEventChanges(flowchart, placements, item_defs): 7 | change_defs = [ 8 | ('fishing-orange', 'Event113', 'Event212'), 9 | ('fishing-cheep-cheep', 'Event3', 'Event10'), 10 | ('fishing-ol-baron', 'Event133', 'Event140'), 11 | ('fishing-50', 'Event182', 'Event240'), 12 | ('fishing-100', 'Event191', 'Event247'), 13 | ('fishing-150', 'Event193', 'Event255'), 14 | ('fishing-loose', 'Event264', 'Event265') 15 | ] 16 | 17 | for defs in change_defs: 18 | item_key = item_defs[placements[defs[0]]]['item-key'] 19 | item_index = placements['indexes'][defs[0]] if defs[0] in placements['indexes'] else -1 20 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, defs[1], defs[2], False, False) 21 | 22 | bottle_get = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 23 | {'symbol': 'FishingBottleGet', 'value': True}, 'Event264') 24 | 25 | event_tools.insertEventAfter(flowchart, 'Event20', 'Event3') 26 | event_tools.insertEventAfter(flowchart, 'Event18', 'Event133') 27 | event_tools.insertEventAfter(flowchart, 'Event24', 'Event191') 28 | event_tools.insertEventAfter(flowchart, 'FishingGetBottle', bottle_get) 29 | 30 | 31 | 32 | def fixFishingBottle(flowchart): 33 | # since no event actually removes sword, we change itemType 0 in RemoveItem to remove Bottle 1 with ASM 34 | take_bottle = event_tools.createActionEvent(flowchart, 'Inventory', 'RemoveItem', 35 | {'itemType': 0}, 'Event74') 36 | fishing_bottle_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 37 | {'symbol': 'FishingBottleGet'}, {0: take_bottle, 1: 'Event83'}) 38 | 39 | event_tools.insertEventAfter(flowchart, 'Event83', 'Event74') 40 | event_tools.insertEventAfter(flowchart, 'Event315', fishing_bottle_check) 41 | event_tools.insertEventAfter(flowchart, 'Event316', fishing_bottle_check) 42 | event_tools.insertEventAfter(flowchart, 'Event317', fishing_bottle_check) 43 | 44 | give_bottle = event_tools.createActionEvent(flowchart, 'Inventory', 'AddBottle', 45 | {'index': 1}, 'Event45') 46 | take_bottle_2 = event_tools.createActionEvent(flowchart, 'Inventory', 'RemoveItem', 47 | {'itemType': 0}, 'Event45') 48 | bottle2_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 49 | {'symbol': 'Bottle2Get'}, {0: take_bottle_2, 1: give_bottle}) 50 | 51 | event_tools.insertEventAfter(flowchart, 'Event189', bottle2_check) 52 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/flags.py: -------------------------------------------------------------------------------- 1 | class GlobalFlags: 2 | def __init__(self, sheet: dict, start_index: int): 3 | self.sheet = sheet 4 | self.index = start_index 5 | self.flags = {} 6 | 7 | 8 | def addFlag(self, name): 9 | self.sheet['values'].append({'symbol': name, 'index': self.index}) 10 | self.flags[name] = self.index 11 | self.index += 1 12 | 13 | 14 | def give_flags(self): 15 | return self.sheet, self.flags 16 | 17 | 18 | 19 | def makeFlags(sheet): 20 | """Appends new flags to the GlobalFlags datasheet to use for Heart Pieces, Instruments, Trade Items, and Companions""" 21 | 22 | global_flags = GlobalFlags(sheet, start_index=1119) 23 | 24 | global_flags.addFlag('AnimalVillageHeartGet') 25 | global_flags.addFlag('AnimalVillageCaveHeartGet') 26 | global_flags.addFlag('TaltalEntranceBlocksHeartGet') 27 | global_flags.addFlag('NorthWastelandHeartGet') 28 | global_flags.addFlag('DesertCaveHeartGet') 29 | global_flags.addFlag('GraveyardCaveHeartGet') 30 | global_flags.addFlag('MabeWellHeartGet') 31 | global_flags.addFlag('UkukuCaveWestHeartGet') 32 | global_flags.addFlag('UkukuCaveEastHeartGet') 33 | global_flags.addFlag('BayPassageHeartGet') 34 | global_flags.addFlag('RiverCrossingHeartGet') 35 | global_flags.addFlag('RapidsWestHeartGet') 36 | global_flags.addFlag('RapidsAscentHeartGet') 37 | global_flags.addFlag('KanaletMoatHeartGet') 38 | global_flags.addFlag('SouthBayHeartGet') 39 | global_flags.addFlag('TaltalCrossingHeartGet') 40 | global_flags.addFlag('TaltalEastHeartGet') 41 | global_flags.addFlag('TaltalWestHeartGet') 42 | global_flags.addFlag('TurtleRockHeartGet') 43 | global_flags.addFlag('PotholeHeartGet') 44 | global_flags.addFlag('WoodsCrossingHeartGet') 45 | global_flags.addFlag('WoodsNorthCaveHeartGet') 46 | global_flags.addFlag('DiamondIslandHeartGet') 47 | 48 | global_flags.addFlag('TailCaveInstrumentGet') 49 | global_flags.addFlag('BottleGrottoInstrumentGet') 50 | global_flags.addFlag('KeyCavernInstrumentGet') 51 | global_flags.addFlag('AnglersTunnelInstrumentGet') 52 | global_flags.addFlag('CatfishsMawInstrumentGet') 53 | global_flags.addFlag('FaceShrineInstrumentGet') 54 | global_flags.addFlag('EaglesTowerInstrumentGet') 55 | global_flags.addFlag('TurtleRockInstrumentGet') 56 | 57 | global_flags.addFlag('TradeYoshiDollGet') 58 | global_flags.addFlag('TradeRibbonGet') 59 | global_flags.addFlag('TradeDogFoodGet') 60 | global_flags.addFlag('TradeBananasGet') 61 | global_flags.addFlag('TradeStickGet') 62 | global_flags.addFlag('TradeHoneycombGet') 63 | global_flags.addFlag('TradePineappleGet') 64 | global_flags.addFlag('TradeHibiscusGet') 65 | global_flags.addFlag('TradeLetterGet') 66 | global_flags.addFlag('TradeBroomGet') 67 | global_flags.addFlag('TradeFishingHookGet') 68 | global_flags.addFlag('TradeNecklaceGet') 69 | global_flags.addFlag('TradeMermaidsScaleGet') 70 | 71 | global_flags.addFlag('KikiGone') 72 | 73 | global_flags.addFlag('PotholeKeySpawn') 74 | 75 | global_flags.addFlag('PrizeGet1') 76 | global_flags.addFlag('PrizeGet2') 77 | global_flags.addFlag('PrizeGet3') 78 | global_flags.addFlag('PrizeGet4') 79 | global_flags.addFlag('PrizeGet5') 80 | global_flags.addFlag('PrizeGet6') 81 | 82 | global_flags.addFlag('Bottle2Get') 83 | global_flags.addFlag('FishingBottleGet') 84 | 85 | global_flags.addFlag('owl-statue-below-D8') 86 | global_flags.addFlag('owl-statue-pothole') 87 | global_flags.addFlag('owl-statue-above-cave') 88 | global_flags.addFlag('owl-statue-moblin-cave') 89 | global_flags.addFlag('owl-statue-south-bay') 90 | global_flags.addFlag('owl-statue-desert') 91 | global_flags.addFlag('owl-statue-maze') 92 | global_flags.addFlag('owl-statue-taltal-east') 93 | global_flags.addFlag('owl-statue-rapids') 94 | 95 | global_flags.addFlag('D1-owl-statue-spinies') 96 | global_flags.addFlag('D1-owl-statue-3-of-a-kind') 97 | global_flags.addFlag('D1-owl-statue-long-hallway') 98 | 99 | global_flags.addFlag('D2-owl-statue-first-switch') 100 | global_flags.addFlag('D2-owl-statue-push-puzzle') 101 | global_flags.addFlag('D2-owl-statue-past-hinox') 102 | 103 | global_flags.addFlag('D3-owl-statue-basement-north') 104 | global_flags.addFlag('D3-owl-statue-arrow') 105 | global_flags.addFlag('D3-owl-statue-northwest') 106 | 107 | global_flags.addFlag('D4-owl-statue') 108 | 109 | global_flags.addFlag('D5-owl-statue-triple-stalfos') 110 | global_flags.addFlag('D5-owl-statue-before-boss') 111 | 112 | global_flags.addFlag('D6-owl-statue-ledge') 113 | global_flags.addFlag('D6-owl-statue-southeast') 114 | global_flags.addFlag('D6-owl-statue-canal') 115 | 116 | global_flags.addFlag('D7-owl-statue-ball') 117 | global_flags.addFlag('D7-owl-statue-kirbys') 118 | global_flags.addFlag('D7-owl-statue-3-of-a-kind-south') 119 | 120 | global_flags.addFlag('D8-owl-statue-above-smasher') 121 | global_flags.addFlag('D8-owl-statue-below-gibdos') 122 | global_flags.addFlag('D8-owl-statue-eye-statue') 123 | 124 | global_flags.addFlag('D0-owl-statue-nine-switches') 125 | global_flags.addFlag('D0-owl-statue-first-switches') 126 | global_flags.addFlag('D0-owl-statue-before-mini-boss') 127 | 128 | global_flags.addFlag('KeyGetField06I') 129 | global_flags.addFlag('KeyGetField06K') 130 | global_flags.addFlag('KeyGetKanalet02A') 131 | global_flags.addFlag('KeyGetKanalet01C') 132 | global_flags.addFlag('KeyGetKanalet01D') 133 | 134 | global_flags.addFlag('FlippersFound') 135 | 136 | global_flags.addFlag('Dampe1') 137 | global_flags.addFlag('DampeHeart') 138 | global_flags.addFlag('Dampe2') 139 | global_flags.addFlag('DampeBottle') 140 | global_flags.addFlag('DampeFinal') 141 | 142 | # global_flags.addFlag('ShopShovelSteal') 143 | # global_flags.addFlag('ShopShovelGet') 144 | # global_flags.addFlag('ShopBowSteal') 145 | # global_flags.addFlag('ShopBowGet') 146 | # global_flags.addFlag('ShopHeartSteal') 147 | # global_flags.addFlag('ShopHeartGet') 148 | 149 | return global_flags.give_flags() 150 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/golden_leaves.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | def addCrowKey(room_data, global_flags: dict): 5 | crow = room_data.actors[0] 6 | crow.parameters[1] = b'' 7 | crow.relationships.y = 1 8 | crow.relationships.section_3 = [8] 9 | 10 | leaf = room_data.actors[6] 11 | leaf.type = 0xa9 # small key 12 | leaf.posX = 131.25 13 | leaf.posZ = 60.75 14 | leaf.switches[0] = (1, 37) # FieldKanaletCrowlyDead 15 | leaf.switches[1] = (1, global_flags['KeyGetField06I']) # index of KeyGetField06I 16 | 17 | checker = copy.deepcopy(room_data.actors[7]) 18 | checker.key = int('A1002A105CF0F2E8', 16) 19 | checker.name = bytes('TagHolocaust-A1002A105CF0F2E8', 'utf-8') 20 | checker.type = 0xf0 21 | checker.parameters = [1, 0, 30, 0, 0, b'', b'', b''] 22 | checker.switches[0] = (1, 37) 23 | checker.switches[1] = (2, 1) 24 | checker.relationships.k = 1 25 | checker.relationships.x = 1 26 | checker.relationships.section_1 = [[[b'', b''], 0]] 27 | room_data.actors.append(checker) 28 | 29 | 30 | 31 | def addBomberKey(room_data, global_flags: dict): 32 | bomber = room_data.actors[0] 33 | bomber.relationships.y = 1 34 | bomber.relationships.section_3 = [3] 35 | 36 | leaf = room_data.actors[2] 37 | leaf.type = 0xa9 # small key 38 | leaf.posX -= 4.5 39 | leaf.posY -= 0.5 40 | leaf.posZ -= 6.0 41 | leaf.switches[0] = (1, 36) # FieldKanaletBombKnuckleDead 42 | leaf.switches[1] = (1, global_flags['KeyGetField06K']) # index of KeyGetField06K 43 | 44 | checker = copy.deepcopy(leaf) 45 | checker.key = int('A1002A205CF0F2E8', 16) 46 | checker.name = bytes('TagHolocaust-A1002A205CF0F2E8', 'utf-8') 47 | checker.type = 0xf0 48 | checker.parameters = [1, 0, 30, 0, 0, b'', b'', b''] 49 | checker.switches[0] = (1, 36) 50 | checker.switches[1] = (2, 1) 51 | checker.relationships.k = 1 52 | checker.relationships.x = 1 53 | checker.relationships.section_1 = [[[b'', b''], 0]] 54 | room_data.actors.append(checker) 55 | 56 | 57 | 58 | def addKillRoomKey(room_data, global_flags: dict): 59 | leaf = room_data.actors[5] 60 | leaf.type = 0xa9 # small key 61 | leaf.switches[1] = (1, global_flags['KeyGetKanalet02A']) # index of KeyGetKanalet02A 62 | 63 | 64 | 65 | def addCrackedWallKey(room_data, global_flags: dict): 66 | enemy = room_data.actors[0] 67 | enemy.parameters[0] = b'' # makes it so it does not drop a golden leaf at all 68 | enemy.relationships.y = 1 69 | enemy.relationships.section_3 = [14] 70 | 71 | leaf = room_data.actors[5] 72 | leaf.type = 0xa9 # small key 73 | leaf.posX = 71.25 74 | leaf.posY = 1.5 75 | leaf.posZ = 1.75 76 | leaf.switches[1] = (1, global_flags['KeyGetKanalet01C']) # index of KeyGetKanalet01C 77 | 78 | checker = copy.deepcopy(leaf) 79 | checker.key = int('A1002A305CF0F2E8', 16) 80 | checker.name = bytes('TagHolocaust-A1002A305CF0F2E8', 'utf-8') 81 | checker.type = 0xf0 82 | checker.parameters = [1, 0, 30, 0, 0, b'', b'', b''] 83 | checker.switches[0] = (1, 649) # GoldenLeafPop_Btl_KanaletCastle_01C 84 | checker.switches[1] = (2, 1) 85 | checker.relationships.k = 1 86 | checker.relationships.x = 1 87 | checker.relationships.section_1 = [[[b'', b''], 0]] 88 | room_data.actors.append(checker) 89 | 90 | 91 | 92 | def addBallChainKey(room_data, global_flags: dict): 93 | key = copy.deepcopy(room_data.actors[0]) 94 | key.key = int('A1002A405CF0F2E8', 16) 95 | key.name = bytes('ItemSmallKey-A1002A405CF0F2E8', 'utf-8') 96 | key.type = 0xa9 # small key 97 | key.posX -= 1.5 98 | key.posZ -= 1.5 99 | key.switches[0] = (1, 650) # GoldenLeafPop_Btl_KanaletCastle_01D 100 | key.switches[1] = (1, global_flags['KeyGetKanalet01D']) # index of KeyGetKanalet01D 101 | room_data.actors.append(key) 102 | 103 | 104 | 105 | def createRoomKey(room_data, room: str, global_flags: dict): 106 | funcs[room](room_data, global_flags) 107 | 108 | 109 | 110 | funcs = { 111 | 'kanalet-crow': addCrowKey, 112 | 'kanalet-mad-bomber': addBomberKey, 113 | 'kanalet-kill-room': addKillRoomKey, 114 | 'kanalet-bombed-guard': addCrackedWallKey, 115 | 'kanalet-final-guard': addBallChainKey 116 | } 117 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/heart_pieces.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | from RandomizerCore.Randomizers.data import HEART_FLAGS, MODEL_SIZES, MODEL_ROTATIONS 4 | 5 | 6 | sunken = [ 7 | 'taltal-east-drop', 8 | 'south-bay-sunken', 9 | 'bay-passage-sunken', 10 | 'river-crossing-cave', 11 | 'kanalet-moat-south' 12 | ] 13 | 14 | 15 | 16 | def changeHeartPiece(flowchart, item_key, item_index, model_path, model_name, room, room_data): 17 | """Applies changes to both the Heart Piece actor and the event flowchart""" 18 | 19 | hp = [a for a in room_data.actors if a.type == 0xB0] 20 | act = hp[0] 21 | 22 | if item_key[:3] == 'Rup': # no need for a fancy animation for rupees, just give them to the player 23 | get_anim = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 24 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 25 | else: 26 | get_anim = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 27 | 28 | event_tools.addEntryPoint(flowchart, room) 29 | event_tools.createActionChain(flowchart, room, [ 30 | ('SinkingSword', 'Destroy', {}), 31 | ('EventFlags', 'SetFlag', {'symbol': HEART_FLAGS[room], 'value': True}) 32 | ], get_anim) 33 | 34 | act.type = 0x194 # sinking sword 35 | 36 | if room in sunken: 37 | if room == 'taltal-east-drop': 38 | act.posY += 2 # cannot see the item in the water, so let's just have it float on the water lol 39 | # else: 40 | # act.posY += 0.5 # raise others up by 1/3 tile 41 | else: 42 | if room == 'mabe-well': 43 | act.posY += 0.5 # this one always ends up clipped into the ground more, so raise by 1/3 tile 44 | else: 45 | act.posY += 0.375 # raise all others by 1/4 tile 46 | 47 | act.parameters[0] = bytes(model_path, 'utf-8') 48 | act.parameters[1] = bytes(model_name, 'utf-8') 49 | act.parameters[2] = bytes(room, 'utf-8') # entry point 50 | act.parameters[3] = bytes(HEART_FLAGS[room], 'utf-8') # flag which controls if the heart piece appears or not 51 | 52 | if item_key == 'Seashell': 53 | act.parameters[4] = bytes('true', 'utf-8') 54 | else: 55 | act.parameters[4] = bytes('false', 'utf-8') 56 | 57 | if model_name in MODEL_SIZES: 58 | size = MODEL_SIZES[model_name] 59 | act.scaleX = size 60 | act.scaleY = size 61 | act.scaleZ = size 62 | if model_name in MODEL_ROTATIONS: 63 | act.rotY = MODEL_ROTATIONS[model_name] 64 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/instruments.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | from RandomizerCore.Randomizers.data import INSTRUMENT_FLAGS, MODEL_SIZES, MODEL_ROTATIONS 4 | import re 5 | 6 | 7 | 8 | def changeInstrument(flowchart, item_key, item_index, model_path, model_name, room, room_data, destination=None): 9 | """Applies changes to both the Instrument actor and the event flowchart""" 10 | 11 | if room == 'D6-instrument': 12 | act = room_data.actors[1] 13 | else: 14 | act = room_data.actors[0] 15 | 16 | if destination is None: 17 | # store the level and location for the leveljump event since we will overwrite these parameters 18 | level = str(act.parameters[0], 'utf-8') 19 | location = str(act.parameters[1], 'utf-8') 20 | else: 21 | level = re.match('(.+)_\\d\\d[A-Z]', destination).group(1) 22 | location = destination 23 | 24 | act.type = 0x8E # yoshi doll, will disappear once you have yoshi, but the player never actually obtains it :) 25 | act.parameters[0] = bytes(model_path, 'utf-8') 26 | act.parameters[1] = bytes(model_name, 'utf-8') 27 | act.parameters[2] = bytes(room, 'utf-8') # entry point that we write to flow 28 | act.parameters[3] = bytes(INSTRUMENT_FLAGS[room], 'utf-8') # flag for if item appears 29 | 30 | if item_key == 'Seashell': 31 | act.parameters[4] = bytes('true', 'utf-8') 32 | else: 33 | act.parameters[4] = bytes('false', 'utf-8') 34 | 35 | if model_name in MODEL_SIZES: 36 | size = MODEL_SIZES[model_name] 37 | act.scaleX = size 38 | act.scaleY = size 39 | act.scaleZ = size 40 | if model_name in MODEL_ROTATIONS: 41 | act.rotY = MODEL_ROTATIONS[model_name] 42 | 43 | fade_event = insertInstrumentFadeEvent(flowchart, level, location) 44 | instrument_get = item_get.insertItemGetAnimation(flowchart, item_key, item_index, None, fade_event) 45 | 46 | event_tools.addEntryPoint(flowchart, room) 47 | event_tools.createActionChain(flowchart, room, [ 48 | ('SinkingSword', 'Destroy', {}), 49 | ('EventFlags', 'SetFlag', {'symbol': INSTRUMENT_FLAGS[room], 'value': True}) 50 | ], instrument_get) 51 | 52 | 53 | 54 | def insertInstrumentFadeEvent(flowchart, level, location): 55 | shine_effect = event_tools.createActionChain(flowchart, None, [ 56 | ('Audio', 'StopAllBGM', {'duration': 1.0}), 57 | ('Link', 'PlayInstrumentShineEffect', {}), 58 | ('Timer', 'Wait', {'time': 2}) 59 | # ('Audio', 'StopOtherThanSystemSE', {'duration': 3.0}), 60 | # ('Audio', 'PlayOneshotSystemSE', {'label': 'SE_ENV_GET_INST_WHITEOUT2', 'pitch': 1.0, 'volume': 1.0}), 61 | # ('Fade', 'StartPreset', {'preset': 3}), 62 | # ('Fade', 'StartParam', {'colorB': 0.9, 'colorG': 0.9, 'colorR': 0.9, 'mode': 2, 'time': 0.75}), 63 | ], None) 64 | 65 | level_jump = event_tools.createActionChain(flowchart, None, [ 66 | ('Timer', 'Wait', {'time': 2}), 67 | ('GameControl', 'RequestLevelJump', {'level': level, 'locator': location, 'offsetX': 0.0, 'offsetZ': 0.0}), 68 | ('GameControl', 'RequestAutoSave', {}) 69 | ], None) 70 | 71 | return event_tools.createForkEvent(flowchart, shine_effect, [ 72 | event_tools.createActionEvent(flowchart, 'Audio', 'StopOtherThanSystemSE', {'duration': 3.0}), 73 | event_tools.createActionEvent(flowchart, 'Audio', 'PlayOneshotSystemSE', {'label': 'SE_ENV_GET_INST_WHITEOUT2', 'pitch': 1.0, 'volume': 1.0}), 74 | event_tools.createActionChain(flowchart, None, [ 75 | ('Fade', 'StartPreset', {'preset': 3}), 76 | ('Fade', 'StartParam', {'colorB': 0.9, 'colorG': 0.9, 'colorR': 0.9, 'mode': 2, 'time': 0.75}) 77 | ]) 78 | ], level_jump)[0] -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/item_drops.py: -------------------------------------------------------------------------------- 1 | from RandomizerCore.Randomizers import data 2 | import RandomizerCore.Tools.oead_tools as oead_tools 3 | import oead 4 | 5 | 6 | 7 | def makeDatasheetChanges(sheet, settings): 8 | """Iterates through all the values in the ItemDrop datasheet and makes changes""" 9 | 10 | for i in range(len(sheet['values'])): 11 | 12 | if sheet['values'][i]['mKey'] == 'HeartContainer0': 13 | first_heart_index = i 14 | 15 | if sheet['values'][i]['mKey'] == 'AnglerKey': 16 | sheet['values'][i]['mLotTable'][0]['mType'] = '' 17 | if sheet['values'][i]['mKey'] == 'FaceKey': 18 | sheet['values'][i]['mLotTable'][0]['mType'] = '' 19 | if sheet['values'][i]['mKey'] == 'HookShot': 20 | sheet['values'][i]['mLotTable'][0]['mType'] = '' 21 | 22 | # Values will be different depending on extended consumable drop and reduce farming settings 23 | if sheet['values'][i]['mKey'] == 'Grass': 24 | heartWeight = sheet['values'][i]['mLotTable'][0]['mWeight'] 25 | rupee1Weight = sheet['values'][i]['mLotTable'][1]['mWeight'] 26 | rupee5Weight = sheet['values'][i]['mLotTable'][2]['mWeight'] 27 | nothingWeight = sheet['values'][i]['mLotTable'][3]['mWeight'] 28 | 29 | # Managing existing entries 30 | rupee1Weight = 18 31 | rupee5Weight = 3 32 | 33 | if settings['extended-consumable-drop']: 34 | nothingWeight = 56 35 | else: 36 | nothingWeight = 71 37 | 38 | sheet['values'][i]['mLotTable'][0]['mWeight'] = heartWeight 39 | sheet['values'][i]['mLotTable'][1]['mWeight'] = rupee1Weight 40 | sheet['values'][i]['mLotTable'][2]['mWeight'] = rupee5Weight 41 | sheet['values'][i]['mLotTable'][3]['mWeight'] = nothingWeight 42 | 43 | # Adding new entries if extended consumable drop setting is enabled 44 | if settings['extended-consumable-drop']: 45 | # Using a copy of an existing entry to use as a skeleton for our new data 46 | dummyEntry = oead_tools.parseStruct(sheet['values'][i]['mLotTable'][0]) 47 | 48 | dummyEntry['mType'] = 'Bomb' 49 | dummyEntry['mWeight'] = 5 50 | sheet['values'][i]['mLotTable'].append(oead_tools.dictToStruct(dummyEntry)) 51 | 52 | dummyEntry['mType'] = 'Arrow' 53 | dummyEntry['mWeight'] = 5 54 | sheet['values'][i]['mLotTable'].append(oead_tools.dictToStruct(dummyEntry)) 55 | 56 | dummyEntry['mType'] = 'MagicPowder' 57 | dummyEntry['mWeight'] = 5 58 | sheet['values'][i]['mLotTable'].append(oead_tools.dictToStruct(dummyEntry)) 59 | 60 | for i in range(8): 61 | sheet['values'][first_heart_index+i]['mLotTable'][0]['mType'] = '' 62 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/mad_batter.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | 3 | 4 | 5 | def writeEvents(flow, item1, item2, item3): 6 | """Combine Talk and End entry points into one flow, cutting out the normal choose your upgrade dialogue. 7 | Then adds separate flows for each Mad Batter to give specific items""" 8 | 9 | event_tools.insertEventAfter(flow.flowchart, 'Event19', 'Event13') 10 | 11 | ## Mad Batter A (bay) 12 | event_tools.addEntryPoint(flow.flowchart, 'BatterA') 13 | subflow_a = event_tools.createSubFlowEvent(flow.flowchart, '', 'talk2', {}) 14 | event_tools.insertEventAfter(flow.flowchart, 'BatterA', subflow_a) 15 | event_tools.insertEventAfter(flow.flowchart, subflow_a, item1) 16 | 17 | ## Mad Batter B (woods) 18 | event_tools.addEntryPoint(flow.flowchart, 'BatterB') 19 | subflow_b = event_tools.createSubFlowEvent(flow.flowchart, '', 'talk2', {}) 20 | event_tools.insertEventAfter(flow.flowchart, 'BatterB', subflow_b) 21 | event_tools.insertEventAfter(flow.flowchart, subflow_b, item2) 22 | 23 | ## Mad Batter C (mountain) 24 | event_tools.addEntryPoint(flow.flowchart, 'BatterC') 25 | subflow_c = event_tools.createSubFlowEvent(flow.flowchart, '', 'talk2', {}) 26 | event_tools.insertEventAfter(flow.flowchart, 'BatterC', subflow_c) 27 | event_tools.insertEventAfter(flow.flowchart, subflow_c, item3) 28 | 29 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/marin.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | 3 | 4 | 5 | def makeEventChanges(flow): 6 | """Removes the event that gives Ballad and edits other events to check if you got the 'song'""" 7 | 8 | fork = event_tools.findEvent(flow.flowchart, 'Event249') 9 | fork.data.forks.pop(0) 10 | event_tools.insertEventAfter(flow.flowchart, 'Event27', 'Event249') 11 | event20 = event_tools.findEvent(flow.flowchart, 'Event20') 12 | event160 = event_tools.findEvent(flow.flowchart, 'Event160') 13 | event676 = event_tools.findEvent(flow.flowchart, 'Event676') 14 | event160.data.actor = event20.data.actor 15 | event676.data.actor = event20.data.actor 16 | event160.data.actor_query = event20.data.actor_query 17 | event676.data.actor_query = event20.data.actor_query 18 | event160.data.params.data['symbol'] = 'MarinsongGet' 19 | event676.data.params.data['symbol'] = 'MarinsongGet' 20 | 21 | # Make Marin not do beach_talk under any circumstance 22 | event_tools.setSwitchEventCase(flow.flowchart, 'Event21', 0, 'Event674') 23 | 24 | # Remove checking for beach item to get song 25 | event_tools.setSwitchEventCase(flow.flowchart, 'Event2', 1, 'Event21') 26 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/miscellaneous.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | from RandomizerCore.Randomizers.data import (BEACH_LOOSE_FLAG, WOODS_LOOSE_FLAG, DREAM_SHRINE_FLAG, 4 | ROOSTER_CAVE_FLAG, MERMAID_CAVE_FLAG, MODEL_SIZES, MODEL_ROTATIONS) 5 | 6 | 7 | def changeSunkenSword(flowchart, item_key, item_index, model_path, model_name, room, music_shuffled): 8 | if music_shuffled: 9 | end_ev = None 10 | del event_tools.findEvent(flowchart, 'Event0').data.forks[0] 11 | else: 12 | end_ev = 'Event8' 13 | 14 | if item_key[:3] == 'Rup': # no need for a fancy animation for rupees, just give them to the player 15 | rup_collect = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 16 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}, end_ev) 17 | event_tools.insertEventAfter(flowchart, 'Event5', rup_collect) 18 | else: 19 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event5', end_ev) 20 | 21 | fork = event_tools.findEvent(flowchart, 'Event0') 22 | fork.data.forks.pop(0) # remove the itemget animation event 23 | event_tools.removeEventAfter(flowchart, 'Event10') 24 | # event_tools.findEvent(flowchart, 'Event1').data.params.data['itemType'] = -1 25 | 26 | fork = event_tools.findEvent(flowchart, 'Event8') 27 | fork.data.forks.pop(1) # remove the sword spin attack animation event 28 | 29 | # update the flag set when getting this item 30 | flag_set = event_tools.findEvent(flowchart, 'Event2') 31 | flag_set.data.params.data['symbol'] = BEACH_LOOSE_FLAG 32 | 33 | # set y-rotation to be 0, if it's something that needs flipped, it will be handled later 34 | act = room.actors[4] 35 | act.rotY = 0.0 36 | 37 | # Keep the normal model if it's a sword 38 | act.parameters[0] = bytes(model_path, 'utf-8') 39 | act.parameters[1] = bytes(model_name, 'utf-8') 40 | act.parameters[2] = bytes('examine', 'utf-8') 41 | act.parameters[3] = bytes(BEACH_LOOSE_FLAG, 'utf-8') 42 | 43 | if item_key == 'Seashell': 44 | act.parameters[4] = bytes('true', 'utf-8') 45 | else: 46 | act.parameters[4] = bytes('false', 'utf-8') 47 | 48 | if model_name in MODEL_SIZES: 49 | size = MODEL_SIZES[model_name] 50 | act.scaleX = size 51 | act.scaleY = size 52 | act.scaleZ = size 53 | if model_name in MODEL_ROTATIONS: 54 | act.rotY = MODEL_ROTATIONS[model_name] 55 | 56 | 57 | def changeMushroom(flowchart, item_key, item_index, model_path, model_name, room): 58 | if item_key[:3] == 'Rup': # no need for a fancy animation for rupees, just give them to the player 59 | get_anim = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 60 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 61 | else: 62 | get_anim = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 63 | 64 | event_tools.addEntryPoint(flowchart, 'Woods') 65 | event_tools.createActionChain(flowchart, 'Woods', [ 66 | ('SinkingSword', 'Destroy', {}), 67 | ('EventFlags', 'SetFlag', {'symbol': WOODS_LOOSE_FLAG, 'value': True}) 68 | ], get_anim) 69 | 70 | act = room.actors[3] 71 | act.type = 0x194 # sinking sword 72 | act.parameters[0] = bytes(model_path, 'utf-8') 73 | act.parameters[1] = bytes(model_name, 'utf-8') 74 | act.parameters[2] = bytes('Woods', 'utf-8') 75 | act.parameters[3] = bytes(WOODS_LOOSE_FLAG, 'utf-8') 76 | 77 | if item_key == 'Seashell': 78 | act.parameters[4] = bytes('true', 'utf-8') 79 | else: 80 | act.parameters[4] = bytes('false', 'utf-8') 81 | 82 | if model_name in MODEL_SIZES: 83 | size = MODEL_SIZES[model_name] 84 | act.scaleX = size 85 | act.scaleY = size 86 | act.scaleZ = size 87 | if model_name in MODEL_ROTATIONS: 88 | act.rotY = MODEL_ROTATIONS[model_name] 89 | 90 | 91 | def changeOcarina(flowchart, item_key, item_index, model_path, model_name, room): 92 | if item_key[:3] == 'Rup': # no need for a fancy animation for rupees, just give them to the player 93 | get_anim = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 94 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 95 | else: 96 | get_anim = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 97 | 98 | event_tools.addEntryPoint(flowchart, 'DreamShrine') 99 | event_tools.createActionChain(flowchart, 'DreamShrine', [ 100 | ('SinkingSword', 'Destroy', {}), 101 | ('EventFlags', 'SetFlag', {'symbol': DREAM_SHRINE_FLAG, 'value': True}) 102 | ], get_anim) 103 | 104 | act = room.actors[5] 105 | act.type = 0x8E # yoshi doll, will disappear once you have yoshi, but the player never actually obtains it :) 106 | act.parameters[0] = bytes(model_path, 'utf-8') 107 | act.parameters[1] = bytes(model_name, 'utf-8') 108 | act.parameters[2] = bytes('DreamShrine', 'utf-8') 109 | act.parameters[3] = bytes(DREAM_SHRINE_FLAG, 'utf-8') # category 1 110 | 111 | if item_key == 'Seashell': 112 | act.parameters[4] = bytes('true', 'utf-8') 113 | else: 114 | act.parameters[4] = bytes('false', 'utf-8') 115 | 116 | if model_name in MODEL_SIZES: 117 | size = MODEL_SIZES[model_name] 118 | act.scaleX = size 119 | act.scaleY = size 120 | act.scaleZ = size 121 | if model_name in MODEL_ROTATIONS: 122 | act.rotY = MODEL_ROTATIONS[model_name] 123 | 124 | 125 | def changeBirdKey(flowchart, item_key, item_index, model_path, model_name, room): 126 | if item_key[:3] == 'Rup': # no need for a fancy animation for rupees, just give them to the player 127 | get_anim = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 128 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 129 | else: 130 | get_anim = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 131 | 132 | event_tools.addEntryPoint(flowchart, 'TalTal') 133 | event_tools.createActionChain(flowchart, 'TalTal', [ 134 | ('SinkingSword', 'Destroy', {}), 135 | ('EventFlags', 'SetFlag', {'symbol': ROOSTER_CAVE_FLAG, 'value': True}) 136 | ], get_anim) 137 | 138 | act = room.actors[0] 139 | act.type = 0x194 # sinking sword 140 | act.parameters[0] = bytes(model_path, 'utf-8') 141 | act.parameters[1] = bytes(model_name, 'utf-8') 142 | act.parameters[2] = bytes('TalTal', 'utf-8') 143 | act.parameters[3] = bytes(ROOSTER_CAVE_FLAG, 'utf-8') 144 | 145 | if item_key == 'Seashell': 146 | act.parameters[4] = bytes('true', 'utf-8') 147 | else: 148 | act.parameters[4] = bytes('false', 'utf-8') 149 | 150 | if model_name in MODEL_SIZES: 151 | size = MODEL_SIZES[model_name] 152 | act.scaleX = size 153 | act.scaleY = size 154 | act.scaleZ = size 155 | if model_name in MODEL_ROTATIONS: 156 | act.rotY = MODEL_ROTATIONS[model_name] 157 | 158 | 159 | def changeLens(flowchart, item_key, item_index, model_path, model_name, room): 160 | if item_key[:3] == 'Rup': # no need for a fancy animation for rupees, just give them to the player 161 | get_anim = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 162 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 163 | else: 164 | get_anim = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 165 | 166 | event_tools.addEntryPoint(flowchart, 'MermaidCave') 167 | event_tools.createActionChain(flowchart, 'MermaidCave', [ 168 | ('SinkingSword', 'Destroy', {}), 169 | ('EventFlags', 'SetFlag', {'symbol': MERMAID_CAVE_FLAG, 'value': True}) 170 | ], get_anim) 171 | 172 | act = room.actors[7] 173 | act.type = 0x194 # sinking sword 174 | act.rotY = 0 # rotate to be facing the screen 175 | act.parameters[0] = bytes(model_path, 'utf-8') 176 | act.parameters[1] = bytes(model_name, 'utf-8') 177 | act.parameters[2] = bytes('MermaidCave', 'utf-8') 178 | act.parameters[3] = bytes(MERMAID_CAVE_FLAG, 'utf-8') 179 | 180 | if item_key == 'Seashell': 181 | act.parameters[4] = bytes('true', 'utf-8') 182 | else: 183 | act.parameters[4] = bytes('false', 'utf-8') 184 | 185 | if model_name in MODEL_SIZES: 186 | size = MODEL_SIZES[model_name] 187 | act.scaleX = size 188 | act.scaleY = size 189 | act.scaleZ = size 190 | if model_name in MODEL_ROTATIONS: 191 | act.rotY = MODEL_ROTATIONS[model_name] 192 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/owls.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def addSlimeKeyCheck(flowchart): 7 | '''Places an item on the owl in front of the slime key regardless if owl gifts are on or not''' 8 | 9 | key_drop = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 10 | {'symbol': 'PotholeKeySpawn', 'value': True}, 'Event21') 11 | shovel_check = event_tools.createSwitchEvent(flowchart, 'Inventory', 'HasItem', 12 | {'itemType': 10, 'count': 1}, {0: 'Event21', 1: key_drop}) 13 | chest_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 14 | {'symbol': 'PotholeKeySpawn'}, {0: shovel_check, 1: 'Event21'}) 15 | event_tools.insertEventAfter(flowchart, 'examine_anaboko', chest_check) 16 | 17 | 18 | 19 | def makeFieldChanges(flowchart, placements, item_defs): 20 | '''Places items on the field owls. Hint system not implemented''' 21 | 22 | field_owls = { 23 | 'owl-statue-below-D8': 'Event20', 24 | 'owl-statue-pothole': 'Event22', 25 | 'owl-statue-above-cave': 'Event28', 26 | 'owl-statue-moblin-cave': 'Event30', 27 | 'owl-statue-south-bay': 'Event32', 28 | 'owl-statue-desert': 'Event34', 29 | 'owl-statue-maze': 'Event36', 30 | 'owl-statue-taltal-east': 'Event38', 31 | 'owl-statue-rapids': 'Event40' 32 | } 33 | for k,v in field_owls.items(): 34 | item = placements[k] 35 | item_index = placements['indexes'][k] if k in placements['indexes'] else -1 36 | 37 | gift_event = item_get.insertItemGetAnimation(flowchart, item_defs[item]['item-key'], item_index, None, None) 38 | 39 | flag_set = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 40 | {'symbol': k, 'value': True}, gift_event) 41 | 42 | flag_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 43 | {'symbol': k}, {0: flag_set, 1: None}) 44 | 45 | event_tools.insertEventAfter(flowchart, v, flag_check) 46 | 47 | # Now lets prevent Link from backing up. This is what causes the get item animation to sometimes not play 48 | dist_evs = ( 49 | 'Event17', 50 | 'Event41', 51 | 'Event42', 52 | 'Event43', 53 | 'Event44', 54 | 'Event45', 55 | 'Event46', 56 | 'Event47', 57 | 'Event48' 58 | ) 59 | for ev in dist_evs: 60 | dist_ev = event_tools.findEvent(flowchart, ev) 61 | dist_ev.data.params.data['keepPersonalSpace'] = False 62 | 63 | 64 | 65 | 66 | def makeDungeonChanges(flowchart, placements, item_defs): 67 | '''Places items on the dungeon owls. Hint system not implemented''' 68 | 69 | ### since Tail Cave statues 04B and 05F use the same event, bundle the event into its own entrypoint 70 | ### then the original entrypoint will be used solely for 05F, and 04B will have its own entrypoint 71 | ### both will subflow to the shared events and then call the item get animation afterwards 72 | ### every other statue event can just add the item event after the message fork events 73 | event_tools.addEntryPoint(flowchart, 'examine_TailShared') 74 | event_tools.insertEventAfter(flowchart, 'examine_TailShared', 'Event22') 75 | 76 | event_tools.addEntryPoint(flowchart, 'examine_Tail04B') 77 | subflow_a = event_tools.createSubFlowEvent(flowchart, '', 'examine_TailShared', {}) 78 | event_tools.insertEventAfter(flowchart, 'examine_Tail04B', subflow_a) 79 | 80 | subflow_b = event_tools.createSubFlowEvent(flowchart, '', 'examine_TailShared', {}) 81 | event_tools.insertEventAfter(flowchart, 'examine_Tail04B05F', subflow_b) 82 | 83 | ### all 3 owl statues in Color Dungeon use the same entrypoint, so same thing here 84 | event_tools.addEntryPoint(flowchart, 'examine_Color06C') # left 85 | subflow_c = event_tools.createSubFlowEvent(flowchart, '', 'examine_Color', {}) 86 | event_tools.insertEventAfter(flowchart, 'examine_Color06C', subflow_c) 87 | 88 | event_tools.addEntryPoint(flowchart, 'examine_Color07D') # center 89 | subflow_d = event_tools.createSubFlowEvent(flowchart, '', 'examine_Color', {}) 90 | event_tools.insertEventAfter(flowchart, 'examine_Color07D', subflow_d) 91 | 92 | event_tools.addEntryPoint(flowchart, 'examine_Color05F') # right 93 | subflow_e = event_tools.createSubFlowEvent(flowchart, '', 'examine_Color', {}) 94 | event_tools.insertEventAfter(flowchart, 'examine_Color05F', subflow_e) 95 | 96 | dungeon_owls = { 97 | 'D1-owl-statue-spinies': subflow_a, 98 | 'D1-owl-statue-3-of-a-kind': subflow_b, 99 | 'D1-owl-statue-long-hallway': 'Event48', 100 | 'D2-owl-statue-first-switch': 'Event59', 101 | 'D2-owl-statue-push-puzzle': 'Event62', 102 | 'D2-owl-statue-past-hinox': 'Event65', 103 | 'D3-owl-statue-basement-north': 'Event27', 104 | 'D3-owl-statue-arrow': 'Event73', 105 | 'D3-owl-statue-northwest': 'Event76', 106 | 'D4-owl-statue': 'Event79', 107 | 'D5-owl-statue-triple-stalfos': 'Event82', 108 | 'D5-owl-statue-before-slime-eel': 'Event85', 109 | 'D6-owl-statue-ledge': 'Event88', 110 | 'D6-owl-statue-southeast': 'Event91', 111 | 'D6-owl-statue-canal': 'Event94', 112 | 'D7-owl-statue-ball': 'Event97', 113 | 'D7-owl-statue-kirbys': 'Event100', 114 | 'D7-owl-statue-hookshot-chest': 'Event103', 115 | 'D8-owl-statue-above-smasher': 'Event39', 116 | 'D8-owl-statue-below-gibdos': 'Event109', 117 | 'D8-owl-statue-eye-statue': 'Event112', 118 | 'D0-owl-statue-nine-switches': subflow_c, 119 | 'D0-owl-statue-first-switches': subflow_d, 120 | 'D0-owl-statue-before-mini-boss': subflow_e 121 | } 122 | for k,v in dungeon_owls.items(): 123 | item = placements[k] 124 | item_index = placements['indexes'][k] if k in placements['indexes'] else -1 125 | 126 | gift_event = item_get.insertItemGetAnimation(flowchart, item_defs[item]['item-key'], item_index, None, None) 127 | 128 | flag_set = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 129 | {'symbol': k, 'value': True}, gift_event) 130 | 131 | flag_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 132 | {'symbol': k}, {0: flag_set, 1: None}) 133 | 134 | event_tools.insertEventAfter(flowchart, v, flag_check) 135 | 136 | # I dont know if the get item animation breaks in dungeons but same thing here as we did with the overworld ones 137 | fork = event_tools.findEvent(flowchart, 'Event50') 138 | fork.data.forks.pop(5) 139 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/player_start.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import data, item_get 3 | 4 | START_FLAGS = ( 5 | 'FirstClear', 6 | 'SecondClear', 7 | 'ThirdClear', 8 | 'FourthClear', 9 | 'FifthClear', 10 | 'SixthClear', 11 | 'SeventhClear', 12 | 'NinthClear', 13 | 'TenthClear', 14 | 'EleventhClear', 15 | 'TwelveClear', 16 | 'ThirteenClear', 17 | 'FourteenClear', 18 | 'FiveteenClear', 19 | 'WalrusAwaked', 20 | 'MarinRescueClear', 21 | 'SwordGet', 22 | 'UI_FieldMapTraverse_MabeVillage', # mabe wont be cleared on the map when you have bowwow for some reason 23 | ) 24 | 25 | BOSS_FLAGS = ( 26 | 'Lv1BossDemoClear', 27 | 'Lv2BossDemoClear', 28 | 'Lv3BossDemoClear', 29 | 'Lv4BossDemoClear', 30 | 'Lv5BossDemoClear', 31 | 'Lv05BrokeWall1', 32 | 'Lv05BrokeWall2', 33 | 'Lv05BrokeWall3', 34 | 'Lv05BrokeWall4', 35 | 'Lv05BrokeFloor', 36 | 'Lv6BossDemoClear', 37 | 'Lv7BossDemoClear', 38 | 'Lv8BossDemoClear', 39 | 'Lv9BossDemoClear', 40 | 'ShadowBattle', 41 | 'LanmolaDemoClear', 42 | 'GrimCreeperDemoClear', 43 | 'StoneHinoxDemoClear', 44 | 'GiantBuzzBlobDemoClear', 45 | 'EvilOrbDemoClear', 46 | 'DeguArmosDemoClear', 47 | 'LanemoraDemoClear' 48 | ) 49 | 50 | MESSAGE_FLAGS = ( 51 | # 'FindWarpPedestalFirst', # excluded because it forces you into the warp 52 | 'FindWarpPointFirst', 53 | 'ArrowGetNoBowMessageShown', 54 | 'MagicPowderFirstMessage', 55 | 'SmallKeyFirstGet' 56 | ) 57 | 58 | 59 | def makeStartChanges(flowchart, settings): 60 | """Sets a bunch of flags when you leave the house for the first time, 61 | including Owl cutscenes watched, Walrus Awakened, and some flags specific to settings""" 62 | 63 | player_start_flags_first_event = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 64 | {'symbol': 'FirstClear', 'value': True}) 65 | player_start_flag_check_event = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 66 | {'symbol': 'FirstClear'}, {0: player_start_flags_first_event, 1: None}) 67 | 68 | player_start_event_flags = list(START_FLAGS) 69 | 70 | if settings['open-kanalet']: 71 | player_start_event_flags.append('GateOpen_Switch_KanaletCastle_01B') 72 | 73 | if settings['open-bridge']: # flag for the bridge, we make kiki use another flag 74 | player_start_event_flags.append('StickDrop') 75 | 76 | if settings['open-mamu']: 77 | player_start_event_flags.append('MamuMazeClear') 78 | 79 | if not settings['shuffle-bombs'] and settings['unlocked-bombs']: 80 | player_start_event_flags.append(data.BOMBS_FOUND_FLAG) 81 | 82 | if settings['randomize-enemies']: # special case where we need stairs under armos to be visible and open 83 | player_start_event_flags.append('AppearStairsFld10N') 84 | player_start_event_flags.append('AppearStairsFld11O') 85 | 86 | if settings['fast-stalfos']: # set the door open flags for the first 3 master stalfos fights to be true 87 | player_start_event_flags.append('DoorOpen_Btl1_L05_05F') 88 | player_start_event_flags.append('DoorOpen_Btl2_L05_04H') 89 | player_start_event_flags.append('DoorOpen_Btl3_L05_01F') 90 | 91 | if settings['boss-cutscenes']: # set boss cutscenes to have already been watched 92 | player_start_event_flags.extend(BOSS_FLAGS) 93 | # if settings['nag-meesages']: # set annoying one-time messages to not pop-up 94 | # player_start_event_flags.extend(MESSAGE_FLAGS) 95 | 96 | player_start_event_flags = [('EventFlags', 'SetFlag', {'symbol': f, 'value': True}) for f in player_start_event_flags] 97 | 98 | event_tools.insertEventAfter(flowchart, 'Event558', player_start_flag_check_event) 99 | event_tools.createActionChain(flowchart, player_start_flags_first_event, player_start_event_flags) 100 | 101 | # Remove the part that kills the rooster after D7 in Level7DungeonIn_FlyingCucco 102 | event_tools.insertEventAfter(flowchart, 'Level7DungeonIn_FlyingCucco', 'Event476') 103 | 104 | if settings['fast-stealing']: 105 | # Remove the flag that says you stole so that the shopkeeper won't kill you 106 | event_tools.createActionChain(flowchart, 'Event774', [ 107 | ('EventFlags', 'SetFlag', {'symbol': 'StealSuccess', 'value': False}) 108 | ]) 109 | 110 | # Auto give dungeon items when entering the dungeon (We have to do that for the level to be identified properly) 111 | dungeon_item_setting = settings['dungeon-items'] 112 | if dungeon_item_setting != 'none': 113 | event_defs = [] 114 | 115 | if dungeon_item_setting in ['mc', 'mcb']: 116 | event_defs += item_get.insertItemWithoutAnimation('DungeonMap', -1) 117 | event_defs += item_get.insertItemWithoutAnimation('Compass', -1) 118 | 119 | if dungeon_item_setting in ['stone-beak', 'mcb']: 120 | event_defs += item_get.insertItemWithoutAnimation('StoneBeak', -1) 121 | 122 | # Connect events to the DungeonIn and DefaultUpStairsOut entrypoints 123 | event_tools.createActionChain(flowchart, 'Event539', event_defs) 124 | event_tools.createActionChain(flowchart, 'Event550', event_defs) 125 | 126 | # Remove the 7 second timeOut wait on the companion when it gets blocked from a loading zone 127 | timeout_events = ('Event637', 'Event660', 'Event693', 'Event696', 'Event371', 'Event407', 'Event478') 128 | for e in timeout_events: 129 | event_tools.findEvent(flowchart, e).data.params.data['timeOut'] = 0.0 130 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/rapids.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def makePrizesStack(flowchart, placements, item_defs): 7 | """Makes the rapids time attack prizes stack, so getting faster times give the slower prizes as well if you do not have them""" 8 | 9 | # 45 prize event doesn't need anything special :) 10 | item_index = placements['indexes']['rapids-race-45'] if 'rapids-race-45' in placements['indexes'] else -1 11 | item_get.insertItemGetAnimation(flowchart, item_defs[placements['rapids-race-45']]['item-key'], item_index, 'Event42', 'Event88') 12 | 13 | # since these events only get called once by using flags, they each can just check the slower goal, and subflow to it 14 | item_index = placements['indexes']['rapids-race-35'] if 'rapids-race-35' in placements['indexes'] else -1 15 | get35 = item_get.insertItemGetAnimation(flowchart, item_defs[placements['rapids-race-35']]['item-key'], item_index, None, 'Event86') 16 | subflow45 = event_tools.createSubFlowEvent(flowchart, '', '5minfirst', {}, get35) 17 | check45 = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 18 | {'symbol': '5minGaul'}, {0: subflow45, 1: get35}) 19 | event_tools.insertEventAfter(flowchart, 'Event40', check45) 20 | 21 | # 30 prize just needs to subflow to the 35 prize, as the 35 prize event already checks for the 45 22 | item_index = placements['indexes']['rapids-race-30'] if 'rapids-race-30' in placements['indexes'] else -1 23 | get30 = item_get.insertItemGetAnimation(flowchart, item_defs[placements['rapids-race-30']]['item-key'], item_index, None, 'Event85') 24 | subflow35 = event_tools.createSubFlowEvent(flowchart, '', '3minfirst', {}, get30) 25 | check35 = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 26 | {'symbol': '3minGaul'}, {0: subflow35, 1: get30}) 27 | event_tools.insertEventAfter(flowchart, 'Event38', check35) 28 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/rupees.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def makeEventChanges(flowchart, rup_index, item_key, item_index): 7 | """Adds an entry point to the flowchart for each rupee, and inserts the ItemGetAnimation event into it""" 8 | 9 | event_tools.addEntryPoint(flowchart, f'Lv10Rupee_{rup_index + 1}') 10 | 11 | # If item is SmallKey/NightmareKey/Map/Compass/Beak/Rupee, add to inventory without any pickup animation 12 | if item_key[:3] in ['Sma', 'Nig', 'Dun', 'Com', 'Sto', 'Rup']: 13 | get_anim = event_tools.createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 14 | {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 15 | else: 16 | get_anim = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 17 | 18 | event_tools.createActionChain(flowchart, f'Lv10Rupee_{rup_index + 1}', [ 19 | ('SinkingSword', 'Destroy', {}), 20 | ('EventFlags', 'SetFlag', {'symbol': 'Lv10RupeeGet' if rup_index == 0 else f'Lv10RupeeGet_{rup_index + 1}', 'value': True}) 21 | ], get_anim) 22 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/shop.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def makeDatasheetChanges(sheet, placements, item_defs): 7 | """Edit the ShopItem datasheet to for the new items. Incomplete, was only testing""" 8 | 9 | for slot in sheet['values']: 10 | if slot['mIndex'] == 2: 11 | item = placements['shop-slot3-1st'] # shovel 12 | slot['mGoods'][0]['mItem'] = 'ShopShovel' 13 | slot['mGoods'][0]['mModelPath'] = f"actor/{item_defs[item]['model-path']}" 14 | slot['mGoods'][0]['mModelName'] = item_defs[item]['model-name'] 15 | slot['mGoods'][0]['mIndex'] = -1 16 | 17 | item = placements['shop-slot3-2nd'] # bow 18 | slot['mGoods'][1]['mItem'] = 'ShopBow' 19 | slot['mGoods'][1]['mModelPath'] = f"actor/{item_defs[item]['model-path']}" 20 | slot['mGoods'][1]['mModelName'] = item_defs[item]['model-name'] 21 | slot['mGoods'][1]['mIndex'] = -1 22 | 23 | if slot['mIndex'] == 5: 24 | item = placements['shop-slot6'] # heart piece 25 | slot['mGoods'][0]['mItem'] = 'ShopHeart' 26 | slot['mGoods'][0]['mModelPath'] = f"actor/{item_defs[item]['model-path']}" 27 | slot['mGoods'][0]['mModelName'] = item_defs[item]['model-name'] 28 | slot['mGoods'][0]['mIndex'] = -1 29 | 30 | 31 | 32 | def makeBuyingEventChanges(flowchart, placements, item_defs): 33 | """edit the ToolShopKeeper event flow for the new items. Incomplete, was only testing""" 34 | 35 | # shovel 36 | item = placements['shop-slot3-1st'] 37 | item_key = item_defs[item]['item-key'] 38 | item_index = placements['indexes']['shop-slot3-1st'] if 'shop-slot3-1st' in placements['indexes'] else -1 39 | event_tools.setSwitchEventCase(flowchart, 'Event50', 1, 'Event52') 40 | event_tools.insertEventAfter(flowchart, 'Event52', 'Event61') 41 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event53', 'Event43') 42 | # event_tools.findEvent(flowchart, 'Event43').data.params.data['symbol'] = 'ShopShovelGet' 43 | 44 | # bow 45 | item = placements['shop-slot3-2nd'] 46 | item_key = item_defs[item]['item-key'] 47 | item_index = placements['indexes']['shop-slot3-2nd'] if 'shop-slot3-2nd' in placements['indexes'] else -1 48 | event_tools.setSwitchEventCase(flowchart, 'Event12', 1, 'Event14') 49 | event_tools.insertEventAfter(flowchart, 'Event14', 'Event65') 50 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event17', 'Event151') 51 | # event_tools.findEvent(flowchart, 'Event151').data.params.data['symbol'] = 'ShopBowGet' 52 | 53 | # heart piece 54 | item = placements['shop-slot6'] 55 | item_key = item_defs[item]['item-key'] 56 | item_index = placements['indexes']['shop-slot6'] if 'shop-slot6' in placements['indexes'] else -1 57 | set_flag = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 58 | {'symbol': 'ShopHeartGet', 'value': True}) 59 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event122', set_flag) 60 | 61 | 62 | 63 | def makeStealingEventChanges(flowchart, placements, item_defs): 64 | # if placements['settings']['fast-stealing']: 65 | # end_event = 'AutoEvent22' 66 | # else: 67 | # end_event = 'Event774' 68 | 69 | remove_heart = event_tools.createActionChain(flowchart, None, [ 70 | ('EventFlags', 'SetFlag', {'symbol': 'ShopHeartGet', 'value': True}), 71 | ('EventFlags', 'SetFlag', {'symbol': 'ShopHeartSteal', 'value': False}), 72 | ], None) 73 | give_heart = item_get.insertItemGetAnimation(flowchart, 74 | item_defs[placements['shop-slot6']]['item-key'], 75 | placements['indexes']['shop-slot6'] if 'shop-slot6' in placements['indexes'] else -1, 76 | before=None, after=remove_heart 77 | ) 78 | check_heart = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 79 | {'symbol': 'ShopHeartSteal'}, {0: None, 1: give_heart}) 80 | 81 | remove_bow = event_tools.createActionChain(flowchart, None, [ 82 | ('EventFlags', 'SetFlag', {'symbol': 'ShopBowGet', 'value': True}), 83 | ('EventFlags', 'SetFlag', {'symbol': 'BowGet', 'value': False}), 84 | ], check_heart) 85 | give_bow = item_get.insertItemGetAnimation(flowchart, 86 | item_defs[placements['shop-slot3-2nd']]['item-key'], 87 | placements['indexes']['shop-slot3-2nd'] if 'shop-slot3-2nd' in placements['indexes'] else -1, 88 | before=None, after=remove_bow 89 | ) 90 | check_bow = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 91 | {'symbol': 'BowGet'}, {0: check_heart, 1: give_bow}) 92 | 93 | remove_shovel = event_tools.createActionChain(flowchart, None, [ 94 | ('EventFlags', 'SetFlag', {'symbol': 'ShopShovelGet', 'value': True}), 95 | ('EventFlags', 'SetFlag', {'symbol': 'ScoopGet', 'value': False}), 96 | ], check_bow) 97 | give_shovel = item_get.insertItemGetAnimation(flowchart, 98 | item_defs[placements['shop-slot3-1st']]['item-key'], 99 | placements['indexes']['shop-slot3-1st'] if 'shop-slot3-1st' in placements['indexes'] else -1, 100 | before=None, after=remove_shovel 101 | ) 102 | check_shovel = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 103 | {'symbol': 'ScoopGet'}, {0: check_bow, 1: give_shovel}) 104 | 105 | event_tools.insertEventAfter(flowchart, 'Event57', check_shovel) 106 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/small_keys.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def writeKeyEvent(flowchart, item_key, item_index, room): 7 | """Adds a new entry point to the SmallKey event flow for each key room, and inserts an ItemGetAnimation to it""" 8 | 9 | # If item is SmallKey/NightmareKey/Map/Compass/Beak/Rupee, add to inventory without any pickup animation 10 | if item_key[:3] in ['Sma', 'Nig', 'Dun', 'Com', 'Sto', 'Rup']: 11 | item_event = event_tools.createActionChain(flowchart, None, [ 12 | ('Inventory', 'AddItemByKey', {'itemKey': item_key, 'count': 1, 'index': item_index, 'autoEquip': False}) 13 | ], None) 14 | else: 15 | item_event = item_get.insertItemGetAnimation(flowchart, item_key, item_index) 16 | 17 | event_tools.addEntryPoint(flowchart, room) 18 | 19 | event_tools.createActionChain(flowchart, room, [ 20 | ('SmallKey', 'Deactivate', {}), 21 | ('SmallKey', 'SetActorSwitch', {'value': True, 'switchIndex': 1}), 22 | ('SmallKey', 'Destroy', {}) 23 | ], item_event) 24 | 25 | 26 | 27 | def makeKeysFaster(flowchart): 28 | '''Gives control back to the player soon after triggering the key to fall''' 29 | 30 | event_tools.insertEventAfter(flowchart, 'pop', 'Event5') 31 | event_tools.insertEventAfter(flowchart, 'Event3', None) 32 | event_tools.findEvent(flowchart, 'Event3').data.params.data['time'] = 2.0 33 | 34 | event_tools.insertEventAfter(flowchart, 'Lv4_04E_pop', 'Event7') 35 | event_tools.insertEventAfter(flowchart, 'Event10', None) 36 | 37 | 38 | 39 | # def writeSunkenKeyEvent(flowchart): 40 | # event_tools.addEntryPoint(flowchart, 'Lv4_04E_pop') 41 | 42 | # event_tools.createActionChain(flowchart, 'Lv4_04E_pop', [ 43 | # ('GoldenLeaf', 'GenericGimmickSequence', {'cameraLookAt': True, 'distanceOffset': 0.0}), 44 | # ('GoldenLeaf', 'Activate', {}), 45 | # ('GoldenLeaf', 'PlayOneshotSE', {'label': 'SE_SY_NAZOKAGI_DROP', 'pitch': 1.0, 'volume': 1.0}), 46 | # ('GoldenLeaf', 'Fall', {}), 47 | # ('Timer', 'Wait', {'time': 2}), 48 | # ('Audio', 'PlayJingle', {'label': 'BGM_NAZOTOKI_SEIKAI', 'volume': 1.0}), 49 | # ('GoldenLeaf', 'Destroy', {}) 50 | # ]) -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/tarin.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | 6 | def makeEventChanges(flowchart, placements, settings, item_defs): 7 | """Edits Tarin to detain you based on if you talked to him rather than on having shield""" 8 | 9 | item_index = placements['indexes']['tarin'] if 'tarin' in placements['indexes'] else -1 10 | item_get.insertItemGetAnimation(flowchart, item_defs[placements['tarin']]['item-key'], item_index, 'Event52', 'Event31') 11 | 12 | # # If reduce-farming is on, and Tarin has boots, also give 20 bombs if Tarin has boots 13 | # if placements['tarin'] == 'boots' and settings['reduce-farming']: 14 | # event_tools.createActionChain(flowchart, 'Event31', [ 15 | # ('Inventory', 'AddItemByKey', {'itemKey': 'Bomb', 'count': 30, 'index': -1, 'autoEquip': False}) 16 | # ], 'Event2') 17 | 18 | event0 = event_tools.findEvent(flowchart, 'Event0') 19 | event78 = event_tools.findEvent(flowchart, 'Event78') 20 | event0.data.actor = event78.data.actor 21 | event0.data.actor_query = event78.data.actor_query 22 | event0.data.params = event78.data.params 23 | 24 | event_defs = [] 25 | sword_num = 0 26 | shield_num = 0 27 | bracelet_num = 0 28 | 29 | for i in placements['starting-items']: 30 | item_key = item_defs[i]['item-key'] 31 | 32 | if item_key == 'SwordLv1': 33 | sword_num += 1 34 | if sword_num == 2: 35 | item_key = 'SwordLv2' 36 | 37 | elif item_key == 'Shield': 38 | shield_num += 1 39 | if shield_num == 2: 40 | item_key = 'MirrorShield' 41 | 42 | elif item_key == 'PowerBraceletLv1': 43 | bracelet_num += 1 44 | if bracelet_num == 2: 45 | item_key = 'PowerBraceletLv2' 46 | 47 | event_defs += item_get.insertItemWithoutAnimation(item_key, -1) 48 | 49 | after = 'Event52' 50 | starting_rupees = settings['starting-rupees'] 51 | if starting_rupees > 0: 52 | event_tools.addActorAction(event_tools.findActor(flowchart, 'Link'), 'AddRupee') 53 | after = event_tools.createActionEvent(flowchart, 'Link', 'AddRupee', {'amount': starting_rupees}, 'Event52') 54 | 55 | if len(event_defs) > 0: 56 | event_tools.createActionChain(flowchart, 'Event36', event_defs, after) 57 | else: 58 | event_tools.insertEventAfter(flowchart, 'Event36', after) 59 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/trade_quest.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get 3 | 4 | 5 | def mamashaChanges(flowchart, item_info): 6 | item_key, item_index = item_info 7 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event15') 8 | 9 | event1 = event_tools.findEvent(flowchart, 'Event1') 10 | event3 = event_tools.findEvent(flowchart, 'Event3') 11 | event3.data.actor = event1.data.actor 12 | event3.data.actor_query = event1.data.actor_query 13 | event3.data.params.data = {'symbol': 'TradeYoshiDollGet'} 14 | 15 | 16 | 17 | def ciaociaoChanges(flowchart, item_info): 18 | item_key, item_index = item_info 19 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event21') 20 | 21 | event1 = event_tools.findEvent(flowchart, 'Event1') 22 | event3 = event_tools.findEvent(flowchart, 'Event3') 23 | event3.data.actor = event1.data.actor 24 | event3.data.actor_query = event1.data.actor_query 25 | event3.data.params.data = {'symbol': 'TradeRibbonGet'} 26 | 27 | 28 | 29 | def saleChanges(flowchart, item_info): 30 | item_key, item_index = item_info 31 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event31') 32 | 33 | event0 = event_tools.findEvent(flowchart, 'Event0') 34 | event2 = event_tools.findEvent(flowchart, 'Event2') 35 | event2.data.actor = event0.data.actor 36 | event2.data.actor_query = event0.data.actor_query 37 | event2.data.params.data = {'symbol': 'TradeDogFoodGet'} 38 | 39 | 40 | 41 | def kikiChanges(flowchart, settings, item_key, item_index): 42 | get_event = item_get.insertItemGetAnimation(flowchart, item_key, item_index, None, 'Event102') 43 | 44 | bananas_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 45 | {'symbol': 'TradeBananasGet'}, {0: 'Event118', 1: 'Event32'}) 46 | 47 | event_tools.insertEventAfter(flowchart, 'Event91', bananas_check) 48 | event_tools.insertEventAfter(flowchart, 'Event84', 'Event15') # skip over setting the trade quest slot to be empty 49 | event_tools.insertEventAfter(flowchart, 'Event29', 'Event88') 50 | fork = event_tools.findEvent(flowchart, 'Event88') 51 | fork.data.forks.pop(0) 52 | 53 | if settings['open-bridge']: 54 | event_tools.insertEventAfter(flowchart, 'Event9', 'Event31') 55 | event_tools.insertEventAfter(flowchart, 'Event10', 'Event31') 56 | 57 | fork = event_tools.findEvent(flowchart, 'Event28') 58 | fork.data.forks.pop(1) 59 | fork.data.forks.pop(1) 60 | fork.data.forks.pop(1) 61 | fork.data.forks.pop(1) 62 | fork.data.forks.pop(2) 63 | 64 | fork = event_tools.findEvent(flowchart, 'Event31') 65 | fork.data.forks.pop(0) 66 | 67 | kiki_gone = event_tools.createActionEvent(flowchart, 'EventFlags', 'SetFlag', 68 | {'symbol': 'KikiGone', 'value': True}, get_event) 69 | 70 | event_tools.insertEventAfter(flowchart, 'Event89', kiki_gone) 71 | else: 72 | event_tools.insertEventAfter(flowchart, 'Event89', get_event) 73 | 74 | 75 | 76 | def tarinChanges(flowchart, item_info): 77 | item_key, item_index = item_info 78 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event130', 'Event29') 79 | 80 | 81 | 82 | def chefChanges(flowchart, item_info): 83 | item_key, item_index = item_info 84 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event16', None) # Event4 85 | 86 | event1 = event_tools.findEvent(flowchart, 'Event1') 87 | event11 = event_tools.findEvent(flowchart, 'Event11') 88 | event11.data.actor = event1.data.actor 89 | event11.data.actor_query = event1.data.actor_query 90 | event11.data.params.data = {'symbol': 'TradeHoneycombGet'} 91 | 92 | 93 | 94 | def papahlChanges(flowchart, item_info): 95 | item_key, item_index = item_info 96 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event32', 'Event62') 97 | 98 | event81 = event_tools.findEvent(flowchart, 'Event81') 99 | event2 = event_tools.findEvent(flowchart, 'Event2') 100 | event2.data.actor = event81.data.actor 101 | event2.data.actor_query = event81.data.actor_query 102 | event2.data.params.data = {'symbol': 'TradePineappleGet'} 103 | 104 | 105 | 106 | def christineChanges(flowchart, item_info): 107 | item_key, item_index = item_info 108 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event15', 'Event22') 109 | 110 | event0 = event_tools.findEvent(flowchart, 'Event0') 111 | event10 = event_tools.findEvent(flowchart, 'Event10') 112 | event10.data.actor = event0.data.actor 113 | event10.data.actor_query = event0.data.actor_query 114 | event10.data.params.data = {'symbol': 'TradeHibiscusGet'} 115 | 116 | event_tools.insertEventAfter(flowchart, 'Event28', 'Event15') 117 | 118 | 119 | 120 | def mrWriteChanges(flowchart, item_info): 121 | item_key, item_index = item_info 122 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event48', 'Event46') 123 | 124 | event0 = event_tools.findEvent(flowchart, 'Event0') 125 | event2 = event_tools.findEvent(flowchart, 'Event2') 126 | event2.data.actor = event0.data.actor 127 | event2.data.actor_query = event0.data.actor_query 128 | event2.data.params.data = {'symbol': 'TradeLetterGet'} 129 | 130 | event_tools.insertEventAfter(flowchart, 'Event7', 'Event47') 131 | 132 | fork = event_tools.findEvent(flowchart, 'Event47') 133 | fork.data.forks.pop(1) 134 | 135 | 136 | 137 | def grandmaYahooChanges(flowchart, item_info): 138 | item_key, item_index = item_info 139 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event54', 'Event33') 140 | 141 | broom_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 142 | {'symbol': 'TradeBroomGet'}, {0: 'Event69', 1: 'Event79'}) 143 | 144 | event_tools.insertEventAfter(flowchart, 'Event11', 'Event0') 145 | 146 | event_tools.setSwitchEventCase(flowchart, 'Event0', 0, broom_check) 147 | 148 | event_tools.insertEventAfter(flowchart, 'Event81', 'Event53') 149 | 150 | fork = event_tools.findEvent(flowchart, 'Event53') 151 | fork.data.forks.pop(1) 152 | 153 | 154 | 155 | def fishermanChanges(flowchart, item_info): 156 | item_key, item_index = item_info 157 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event28', 'Event42') 158 | 159 | event0 = event_tools.findEvent(flowchart, 'Event0') 160 | event2 = event_tools.findEvent(flowchart, 'Event2') 161 | event2.data.actor = event0.data.actor 162 | event2.data.actor_query = event0.data.actor_query 163 | event2.data.params.data = {'symbol': 'TradeFishingHookGet'} 164 | 165 | event_tools.insertEventAfter(flowchart, 'Event32', 'Event33') 166 | 167 | fork = event_tools.findEvent(flowchart, 'Event27') 168 | fork.data.forks.pop(1) 169 | 170 | 171 | 172 | def mermaidChanges(flowchart, item_info): 173 | item_key, item_index = item_info 174 | item_get.insertItemGetAnimation(flowchart, item_key, item_index, 'Event73', 'Event55') 175 | 176 | event0 = event_tools.findEvent(flowchart, 'Event0') 177 | event2 = event_tools.findEvent(flowchart, 'Event2') 178 | event2.data.actor = event0.data.actor 179 | event2.data.actor_query = event0.data.actor_query 180 | event2.data.params.data = {'symbol': 'TradeNecklaceGet'} 181 | 182 | fork = event_tools.findEvent(flowchart, 'Event71') 183 | fork.data.forks.pop(1) 184 | 185 | 186 | 187 | def statueChanges(flowchart): 188 | scale_check = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 189 | {'symbol': 'TradeMermaidsScaleGet'}, {0: 'Event28', 1: 'Event32'}) 190 | event_tools.setSwitchEventCase(flowchart, 'Event3', 0, scale_check) 191 | -------------------------------------------------------------------------------- /RandomizerCore/Randomizers/tunic_swap.py: -------------------------------------------------------------------------------- 1 | import RandomizerCore.Tools.event_tools as event_tools 2 | from RandomizerCore.Randomizers import item_get, data 3 | 4 | 5 | 6 | def writeSwapEvents(flowchart): 7 | """Makes the telephone pickup event to basically just be the Fairy Queen and lets you swap tunics""" 8 | 9 | # telephone needs dialog query 'GetLastResult4' to get dialog result 10 | event_tools.addActorQuery(event_tools.findActor(flowchart, 'Dialog'), 'GetLastResult4') 11 | 12 | green_get = item_get.insertItemGetAnimation(flowchart, 'ClothesGreen', -1, None, None) 13 | 14 | red_get = item_get.insertItemGetAnimation(flowchart, 'ClothesRed', -1, None, None) 15 | check_red = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 16 | {'symbol': data.RED_TUNIC_FOUND_FLAG}, {0: None, 1: red_get}) 17 | 18 | blue_get = item_get.insertItemGetAnimation(flowchart, 'ClothesBlue', -1, None, None) 19 | check_blue = event_tools.createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 20 | {'symbol': data.BLUE_TUNIC_FOUND_FLAG}, {0: None, 1: blue_get}) 21 | 22 | get_red_blue = hasGreenEvent(flowchart, check_red, check_blue) 23 | get_blue_green = hasRedEvent(flowchart, check_blue, green_get) 24 | get_red_green = hasBlueEvent(flowchart, check_red, green_get) 25 | 26 | tunic_blue = event_tools.createSwitchEvent(flowchart, 'Inventory', 'HasItem', {'count': 1, 'itemType': 20}, {0: get_red_blue, 1: get_red_green}) 27 | tunic_red = event_tools.createSwitchEvent(flowchart, 'Inventory', 'HasItem', {'count': 1, 'itemType': 19}, {0: tunic_blue, 1: get_blue_green}) 28 | 29 | greeting = event_tools.createActionEvent(flowchart, 'Telephone', 'Examine', {'message': 'SubEvent:QuestGrandFairy7'}, tunic_red) 30 | 31 | event_tools.insertEventAfter(flowchart, 'Telephone', greeting) 32 | 33 | fork = event_tools.findEvent(flowchart, 'Event231') 34 | fork.data.forks.pop(2) # remove the gethints event 35 | 36 | sub_flow = event_tools.createSubFlowEvent(flowchart, '', 'Telephone', {}, 'Event113') 37 | event_tools.insertEventAfter(flowchart, 'Event98', sub_flow) 38 | 39 | 40 | 41 | def hasGreenEvent(flowchart, red, blue): 42 | """If the player currently has the green tunic, create and return the dialog event for swapping between red and blue""" 43 | 44 | dialog_result = event_tools.createSwitchEvent(flowchart, 'Dialog', 'GetLastResult4', {}, {0: red, 1: blue, 2: None}) 45 | return event_tools.createActionEvent(flowchart, 'Telephone', 'Examine', {'message': 'SubEvent:QuestGrandFairy1_2'}, dialog_result) 46 | 47 | 48 | def hasRedEvent(flowchart, blue, green): 49 | """If the player currently has the red tunic, create and return the dialog event for swapping between blue and green""" 50 | 51 | dialog_result = event_tools.createSwitchEvent(flowchart, 'Dialog', 'GetLastResult4', {}, {0: blue, 1: green, 2: None}) 52 | return event_tools.createActionEvent(flowchart, 'Telephone', 'Examine', {'message': 'SubEvent:QuestGrandFairy1_4'}, dialog_result) 53 | 54 | 55 | def hasBlueEvent(flowchart, red, green): 56 | """If the player currently has the blue tunic, create and return the dialog event for swapping between red and green""" 57 | 58 | dialog_result = event_tools.createSwitchEvent(flowchart, 'Dialog', 'GetLastResult4', {}, {0: red, 1: green, 2: None}) 59 | return event_tools.createActionEvent(flowchart, 'Telephone', 'Examine', {'message': 'SubEvent:QuestGrandFairy1_3'}, dialog_result) 60 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/bntx_editor/bfres.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import struct 5 | from copy import copy 6 | 7 | from RandomizerCore.Tools.bntx_editor.structs import (BFRESHeader, BlockHeader, RelocTBL) 8 | 9 | class BfresTexture: 10 | def __init__(self, bfresFile): 11 | self.size = None 12 | self.pos = None 13 | self.bfresFile = bfresFile 14 | 15 | def load(self): 16 | for blockIndex, block in enumerate(self.bfresFile.relocTbl.blocks): 17 | blockMagic = struct.unpack( 18 | self.bfresFile.header.endianness + "4s", 19 | self.bfresFile.rawData[block.pos: block.pos + 4] 20 | )[0] 21 | 22 | if blockMagic == b'BNTX': 23 | self.pos = block.pos 24 | self.size = block.size_ 25 | break 26 | 27 | 28 | class File: 29 | def __init__(self): 30 | self.bfresTexture = None 31 | self.relocTbl = None 32 | self.relocTblHeader = None 33 | self.header = None 34 | self.rawData = None 35 | 36 | def readFromFile(self, fname): 37 | with open(fname, "rb") as inf: 38 | inb = inf.read() 39 | 40 | return self.load(inb, 0) 41 | 42 | def load(self, data, pos): 43 | self.header = BFRESHeader() 44 | returnCode = self.header.load(data, pos) 45 | if returnCode: 46 | raise Exception("A problem occured while loading the BFRES file header") 47 | 48 | pos = self.header.relocAddr 49 | self.relocTblHeader = BlockHeader(self.header.endianness) 50 | self.relocTblHeader.load(data, pos) 51 | returnCode = self.relocTblHeader.isValid(b'_RLT') 52 | if returnCode: 53 | raise Exception("A problem occured while loading the BFRES relocation table header") 54 | 55 | self.relocTbl = RelocTBL(self.header.endianness) 56 | self.relocTbl.load(data, pos + 16, self.relocTblHeader.blockSize) 57 | 58 | self.rawData = copy(data) 59 | 60 | self.bfresTexture = BfresTexture(self) 61 | self.bfresTexture.load() 62 | return 0 63 | 64 | def extractMainBNTX(self, outFile): 65 | if self.bfresTexture.pos is None: 66 | raise Exception("No BNTX File found in this BFRES file") 67 | 68 | # TODO Find out why block's size gives something bigger (there is a padding). 69 | bntxContent = self.rawData[self.bfresTexture.pos: self.bfresTexture.pos + self.bfresTexture.size] 70 | with open(outFile, 'wb') as f: 71 | f.write(bntxContent) 72 | 73 | def replaceMainBNTX(self, inputFile): 74 | if self.bfresTexture.pos is None: 75 | raise Exception("No BNTX File to replace") 76 | 77 | # TODO / WARNING This is a byte-to-byte replacement. This might not work in every case. If size differs 78 | # Some parts might be broken 79 | with open(inputFile, "rb") as injectF: 80 | injectB = injectF.read() 81 | inb = bytearray(self.rawData) 82 | size = os.path.getsize(inputFile) 83 | inb[self.bfresTexture.pos: self.bfresTexture.pos + size] = injectB 84 | self.rawData = inb 85 | 86 | 87 | def saveAs(self, outFile): 88 | with open(outFile, "wb") as out: 89 | out.write(self.rawData) -------------------------------------------------------------------------------- /RandomizerCore/Tools/bntx_editor/bntx_editor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed under GNU GPLv3 4 | 5 | import RandomizerCore.Tools.bntx_editor.bntx as BNTX 6 | from io import BytesIO 7 | 8 | 9 | class BNTXEditor: 10 | def __init__(self): 11 | super().__init__() 12 | self.bntx = BNTX.File() 13 | 14 | def openFile(self, data): 15 | returnCode = self.bntx.readBytes(data) 16 | if returnCode: 17 | return False 18 | 19 | def exportTexByIndex(self, index) -> bytes: 20 | return self.bntx.extract(index) 21 | 22 | def replaceTexByIndex(self, file, index): 23 | if not file: 24 | return False 25 | texture = self.bntx.textures[index] 26 | texture_ = self.bntx.replace(texture, texture.tileMode, False, False, False, True, file) 27 | if texture_: 28 | self.bntx.textures[index] = texture_ 29 | 30 | def save(self): 31 | return self.bntx.save() 32 | 33 | def replaceTextureByName(self, textureName, textureFile): 34 | # Get Texture Index by Name 35 | foundIndex = -1 36 | for imageIndex, element in enumerate(self.bntx.textures): 37 | if element.name == textureName: 38 | foundIndex = imageIndex 39 | break 40 | 41 | if foundIndex < 0: 42 | raise Exception(f'Texture {textureName} not found') 43 | 44 | # Inject it back to the BNTX File 45 | if isinstance(textureFile, str): 46 | with open(textureFile, "rb") as textureFileInstance: 47 | self.replaceTexByIndex(textureFileInstance, foundIndex) 48 | elif isinstance(textureFile, BytesIO): 49 | self.replaceTexByIndex(textureFile, foundIndex) 50 | else: 51 | raise Exception("textureFile is an unknown type") 52 | 53 | def saveAs(self, file): 54 | if not file: 55 | return False 56 | 57 | with open(file, "wb") as out: 58 | out.write(self.bntx.save()) -------------------------------------------------------------------------------- /RandomizerCore/Tools/bntx_editor/formConv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of the BNTX Editor made by AboodXD 5 | # Original repository : https://github.com/aboood40091/BNTX-Editor 6 | # Licensed under GNU GPLv3 7 | 8 | ################################################################ 9 | ################################################################ 10 | 11 | 12 | def getComponentsFromPixel(format_, pixel, comp): 13 | if format_ == 'l8': 14 | comp[2] = pixel & 0xFF 15 | 16 | elif format_ == 'la8': 17 | comp[2] = pixel & 0xFF 18 | comp[3] = (pixel & 0xFF00) >> 8 19 | 20 | elif format_ == 'la4': 21 | comp[2] = (pixel & 0xF) * 17 22 | comp[3] = ((pixel & 0xF0) >> 4) * 17 23 | 24 | elif format_ == 'rgb565': 25 | comp[2] = int((pixel & 0x1F) / 0x1F * 0xFF) 26 | comp[3] = int(((pixel & 0x7E0) >> 5) / 0x3F * 0xFF) 27 | comp[4] = int(((pixel & 0xF800) >> 11) / 0x1F * 0xFF) 28 | 29 | elif format_ == 'bgr565': 30 | comp[2] = int(((pixel & 0xF800) >> 11) / 0x1F * 0xFF) 31 | comp[3] = int(((pixel & 0x7E0) >> 5) / 0x3F * 0xFF) 32 | comp[4] = int((pixel & 0x1F) / 0x1F * 0xFF) 33 | 34 | elif format_ == 'rgb5a1': 35 | comp[2] = int((pixel & 0x1F) / 0x1F * 0xFF) 36 | comp[3] = int(((pixel & 0x3E0) >> 5) / 0x1F * 0xFF) 37 | comp[4] = int(((pixel & 0x7c00) >> 10) / 0x1F * 0xFF) 38 | comp[5] = ((pixel & 0x8000) >> 15) * 0xFF 39 | 40 | elif format_ == 'bgr5a1': 41 | comp[2] = int(((pixel & 0x7c00) >> 10) / 0x1F * 0xFF) 42 | comp[3] = int(((pixel & 0x3E0) >> 5) / 0x1F * 0xFF) 43 | comp[4] = int((pixel & 0x1F) / 0x1F * 0xFF) 44 | comp[5] = ((pixel & 0x8000) >> 15) * 0xFF 45 | 46 | elif format_ == 'a1bgr5': 47 | comp[2] = ((pixel & 0x8000) >> 15) * 0xFF 48 | comp[3] = int(((pixel & 0x7c00) >> 10) / 0x1F * 0xFF) 49 | comp[4] = int(((pixel & 0x3E0) >> 5) / 0x1F * 0xFF) 50 | comp[5] = int((pixel & 0x1F) / 0x1F * 0xFF) 51 | 52 | elif format_ == 'rgba4': 53 | comp[2] = (pixel & 0xF) * 17 54 | comp[3] = ((pixel & 0xF0) >> 4) * 17 55 | comp[4] = ((pixel & 0xF00) >> 8) * 17 56 | comp[5] = ((pixel & 0xF000) >> 12) * 17 57 | 58 | elif format_ == 'abgr4': 59 | comp[2] = ((pixel & 0xF000) >> 12) * 17 60 | comp[3] = ((pixel & 0xF00) >> 8) * 17 61 | comp[4] = ((pixel & 0xF0) >> 4) * 17 62 | comp[5] = (pixel & 0xF) * 17 63 | 64 | elif format_ == 'rgb8': 65 | comp[2] = pixel & 0xFF 66 | comp[3] = (pixel & 0xFF00) >> 8 67 | comp[4] = (pixel & 0xFF0000) >> 16 68 | 69 | elif format_ == 'bgr10a2': 70 | comp[2] = int((pixel & 0x3FF) / 0x3FF * 0xFF) 71 | comp[3] = int(((pixel & 0xFFC00) >> 10) / 0x3FF * 0xFF) 72 | comp[4] = int(((pixel & 0x3FF00000) >> 20) / 0x3FF * 0xFF) 73 | comp[5] = int(((pixel & 0xC0000000) >> 30) / 0x3 * 0xFF) 74 | 75 | elif format_ == 'rgba8': 76 | comp[2] = pixel & 0xFF 77 | comp[3] = (pixel & 0xFF00) >> 8 78 | comp[4] = (pixel & 0xFF0000) >> 16 79 | comp[5] = (pixel & 0xFF000000) >> 24 80 | 81 | elif format_ == 'bgra8': 82 | comp[2] = (pixel & 0xFF0000) >> 16 83 | comp[3] = (pixel & 0xFF00) >> 8 84 | comp[4] = pixel & 0xFF 85 | comp[5] = (pixel & 0xFF000000) >> 24 86 | 87 | return comp 88 | 89 | def torgba8(width, height, data, format_, bpp, compSel): 90 | size = width * height * 4 91 | assert len(data) >= width * height * bpp 92 | 93 | new_data = bytearray(size) 94 | comp = [0, 0xFF, 0, 0, 0, 0xFF] 95 | 96 | if bpp not in [1, 2, 4]: 97 | return new_data 98 | 99 | for y in range(height): 100 | for x in range(width): 101 | pos = (y * width + x) * bpp 102 | pos_ = (y * width + x) * 4 103 | 104 | pixel = 0 105 | for i in range(bpp): 106 | pixel |= data[pos + i] << (8 * i) 107 | 108 | comp = getComponentsFromPixel(format_, pixel, comp) 109 | 110 | new_data[pos_ + 3] = comp[compSel[3]] 111 | new_data[pos_ + 2] = comp[compSel[2]] 112 | new_data[pos_ + 1] = comp[compSel[1]] 113 | new_data[pos_ + 0] = comp[compSel[0]] 114 | 115 | return bytes(new_data) 116 | 117 | 118 | def rgb8torgbx8(data): 119 | numPixels = len(data) // 3 120 | 121 | new_data = bytearray(numPixels * 4) 122 | 123 | for i in range(numPixels): 124 | new_data[4 * i + 0] = data[3 * i + 0] 125 | new_data[4 * i + 1] = data[3 * i + 1] 126 | new_data[4 * i + 2] = data[3 * i + 2] 127 | new_data[4 * i + 3] = 0xFF 128 | 129 | return bytes(new_data) 130 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/bntx_editor/globals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of the BNTX Editor made by AboodXD 5 | # Original repository : https://github.com/aboood40091/BNTX-Editor 6 | 7 | # BNTX Editor is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # BNTX Editor is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | Version = '0.3' 21 | 22 | formats = { 23 | 0x0101: 'R4_G4_UNORM', 24 | 0x0201: 'R8_UNORM', 25 | 0x0301: 'R4_G4_B4_A4_UNORM', 26 | 0x0401: 'A4_B4_G4_R4_UNORM', 27 | 0x0501: 'R5_G5_B5_A1_UNORM', 28 | 0x0601: 'A1_B5_G5_R5_UNORM', 29 | 0x0701: 'R5_G6_B5_UNORM', 30 | 0x0801: 'B5_G6_R5_UNORM', 31 | 0x0901: 'R8_G8_UNORM', 32 | 0x0b01: 'R8_G8_B8_A8_UNORM', 33 | 0x0b06: 'R8_G8_B8_A8_SRGB', 34 | 0x0c01: 'B8_G8_R8_A8_UNORM', 35 | 0x0c06: 'B8_G8_R8_A8_SRGB', 36 | 0x0e01: 'R10_G10_B10_A2_UNORM', 37 | 0x1a01: 'BC1_UNORM', 38 | 0x1a06: 'BC1_SRGB', 39 | 0x1b01: 'BC2_UNORM', 40 | 0x1b06: 'BC2_SRGB', 41 | 0x1c01: 'BC3_UNORM', 42 | 0x1c06: 'BC3_SRGB', 43 | 0x1d01: 'BC4_UNORM', 44 | 0x1d02: 'BC4_SNORM', 45 | 0x1e01: 'BC5_UNORM', 46 | 0x1e02: 'BC5_SNORM', 47 | 0x1f05: 'BC6_FLOAT', 48 | 0x1f0a: 'BC6_UFLOAT', 49 | 0x2001: 'BC7_UNORM', 50 | 0x2006: 'BC7_SRGB', 51 | 0x2d01: 'ASTC_4x4_UNORM', 52 | 0x2d06: 'ASTC_4x4_SRGB', 53 | 0x2e01: 'ASTC_5x4_UNORM', 54 | 0x2e06: 'ASTC_5x4_SRGB', 55 | 0x2f01: 'ASTC_5x5_UNORM', 56 | 0x2f06: 'ASTC_5x5_SRGB', 57 | 0x3001: 'ASTC_6x5_UNORM', 58 | 0x3006: 'ASTC_6x5_SRGB', 59 | 0x3101: 'ASTC_6x6_UNORM', 60 | 0x3106: 'ASTC_6x6_SRGB', 61 | 0x3201: 'ASTC_8x5_UNORM', 62 | 0x3206: 'ASTC_8x5_SRGB', 63 | 0x3301: 'ASTC_8x6_UNORM', 64 | 0x3306: 'ASTC_8x6_SRGB', 65 | 0x3401: 'ASTC_8x8_UNORM', 66 | 0x3406: 'ASTC_8x8_SRGB', 67 | 0x3501: 'ASTC_10x5_UNORM', 68 | 0x3506: 'ASTC_10x5_SRGB', 69 | 0x3601: 'ASTC_10x6_UNORM', 70 | 0x3606: 'ASTC_10x6_SRGB', 71 | 0x3701: 'ASTC_10x8_UNORM', 72 | 0x3706: 'ASTC_10x8_SRGB', 73 | 0x3801: 'ASTC_10x10_UNORM', 74 | 0x3806: 'ASTC_10x10_SRGB', 75 | 0x3901: 'ASTC_12x10_UNORM', 76 | 0x3906: 'ASTC_12x10_SRGB', 77 | 0x3a01: 'ASTC_12x12_UNORM', 78 | 0x3a06: 'ASTC_12x12_SRGB', 79 | 0x3b01: 'B5_G5_R5_A1_UNORM', 80 | } 81 | 82 | targets = [ 83 | "PC (Gen)", 84 | "Switch (NX)", 85 | ] 86 | 87 | accessFlags = [ 88 | "Read", "Write", 89 | "VertexBuffer", "IndexBuffer", 90 | "ConstantBuffer", "Texture", 91 | "UnorderedAccessBuffer", "ColorBuffer", 92 | "DepthStencil", "IndirectBuffer", 93 | "ScanBuffer", "QueryBuffer", 94 | "Descriptor", "ShaderCode", 95 | "Image", 96 | ] 97 | 98 | dims = [ 99 | "Undefined", "1D", 100 | "2D", "3D", 101 | ] 102 | 103 | imgDims = [ 104 | "1D", "2D", "3D", 105 | "Cube", "1D Array", "2D Array", 106 | "2D Multisample", "2D Multisample Array", 107 | "Cube Array", 108 | ] 109 | 110 | tileModes = { 111 | 0: "Optimal", 112 | 1: "Linear", 113 | } 114 | 115 | compSels = [ 116 | "Zero", "One", "Red", 117 | "Green", "Blue", "Alpha", 118 | ] 119 | 120 | BCn_formats = [ 121 | 0x1a, 0x1b, 0x1c, 0x1d, 122 | 0x1e, 0x1f, 0x20, 123 | ] 124 | 125 | ASTC_formats = [ 126 | 0x2d, 0x2e, 0x2f, 0x30, 127 | 0x31, 0x32, 0x33, 0x34, 128 | 0x35, 0x36, 0x37, 0x38, 129 | 0x39, 0x3a, 130 | ] 131 | 132 | blk_dims = { # format -> (blkWidth, blkHeight) 133 | 0x1a: (4, 4), 0x1b: (4, 4), 0x1c: (4, 4), 134 | 0x1d: (4, 4), 0x1e: (4, 4), 0x1f: (4, 4), 135 | 0x20: (4, 4), 0x2d: (4, 4), 0x2e: (5, 4), 136 | 0x2f: (5, 5), 0x30: (6, 5), 137 | 0x31: (6, 6), 0x32: (8, 5), 138 | 0x33: (8, 6), 0x34: (8, 8), 139 | 0x35: (10, 5), 0x36: (10, 6), 140 | 0x37: (10, 8), 0x38: (10, 10), 141 | 0x39: (12, 10), 0x3a: (12, 12), 142 | } 143 | 144 | bpps = { # format -> bytes_per_pixel 145 | 0x01: 0x01, 0x02: 0x01, 0x03: 0x02, 0x04: 0x02, 0x05: 0x02, 0x06: 0x02, 146 | 0x07: 0x02, 0x08: 0x02, 0x09: 0x02, 0x0b: 0x04, 0x0c: 0x04, 0x0e: 0x04, 147 | 0x1a: 0x08, 0x1b: 0x10, 0x1c: 0x10, 0x1d: 0x08, 0x1e: 0x10, 0x1f: 0x10, 148 | 0x20: 0x10, 0x2d: 0x10, 0x2e: 0x10, 0x2f: 0x10, 0x30: 0x10, 0x31: 0x10, 149 | 0x32: 0x10, 0x33: 0x10, 0x34: 0x10, 0x35: 0x10, 0x36: 0x10, 0x37: 0x10, 150 | 0x38: 0x10, 0x39: 0x10, 0x3a: 0x10, 0x3b: 0x02, 151 | } 152 | 153 | 154 | fileData = bytearray() 155 | texSizes = [] -------------------------------------------------------------------------------- /RandomizerCore/Tools/bntx_editor/swizzle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of the BNTX Editor made by AboodXD 5 | # Original repository : https://github.com/aboood40091/BNTX-Editor 6 | 7 | # BNTX Editor is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # BNTX Editor is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | def DIV_ROUND_UP(n, d): 22 | return (n + d - 1) // d 23 | 24 | 25 | def round_up(x, y): 26 | return ((x - 1) | (y - 1)) + 1 27 | 28 | 29 | def pow2_round_up(x): 30 | x -= 1 31 | x |= x >> 1 32 | x |= x >> 2 33 | x |= x >> 4 34 | x |= x >> 8 35 | x |= x >> 16 36 | 37 | return x + 1 38 | 39 | 40 | def getBlockHeight(height): 41 | blockHeight = pow2_round_up(height // 8) 42 | if blockHeight > 16: 43 | blockHeight = 16 44 | 45 | return blockHeight 46 | 47 | 48 | def _swizzle(width, height, blkWidth, blkHeight, roundPitch, bpp, tileMode, blockHeightLog2, data, toSwizzle): 49 | assert 0 <= blockHeightLog2 <= 5 50 | blockHeight = 1 << blockHeightLog2 51 | 52 | width = DIV_ROUND_UP(width, blkWidth) 53 | height = DIV_ROUND_UP(height, blkHeight) 54 | 55 | if tileMode == 1: 56 | pitch = width * bpp 57 | 58 | if roundPitch: 59 | pitch = round_up(pitch, 32) 60 | 61 | surfSize = pitch * height 62 | 63 | else: 64 | pitch = round_up(width * bpp, 64) 65 | surfSize = pitch * round_up(height, blockHeight * 8) 66 | 67 | result = bytearray(surfSize) 68 | 69 | for y in range(height): 70 | for x in range(width): 71 | if tileMode == 1: 72 | pos = y * pitch + x * bpp 73 | 74 | else: 75 | pos = getAddrBlockLinear(x, y, width, bpp, 0, blockHeight) 76 | 77 | pos_ = (y * width + x) * bpp 78 | 79 | if pos + bpp <= surfSize: 80 | if toSwizzle: 81 | result[pos:pos + bpp] = data[pos_:pos_ + bpp] 82 | 83 | else: 84 | result[pos_:pos_ + bpp] = data[pos:pos + bpp] 85 | 86 | return result 87 | 88 | 89 | def deswizzle(width, height, blkWidth, blkHeight, roundPitch, bpp, tileMode, blockHeightLog2, data): 90 | return _swizzle(width, height, blkWidth, blkHeight, roundPitch, bpp, tileMode, blockHeightLog2, bytes(data), 0) 91 | 92 | 93 | def swizzle(width, height, blkWidth, blkHeight, roundPitch, bpp, tileMode, blockHeightLog2, data): 94 | return _swizzle(width, height, blkWidth, blkHeight, roundPitch, bpp, tileMode, blockHeightLog2, bytes(data), 1) 95 | 96 | 97 | def getAddrBlockLinear(x, y, image_width, bytes_per_pixel, base_address, blockHeight): 98 | """ 99 | From the Tegra X1 TRM 100 | """ 101 | image_width_in_gobs = DIV_ROUND_UP(image_width * bytes_per_pixel, 64) 102 | 103 | GOB_address = (base_address 104 | + (y // (8 * blockHeight)) * 512 * blockHeight * image_width_in_gobs 105 | + (x * bytes_per_pixel // 64) * 512 * blockHeight 106 | + (y % (8 * blockHeight) // 8) * 512) 107 | 108 | x *= bytes_per_pixel 109 | 110 | Address = (GOB_address + ((x % 64) // 32) * 256 + ((y % 8) // 2) * 64 111 | + ((x % 32) // 16) * 32 + (y % 2) * 16 + (x % 16)) 112 | 113 | return Address 114 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/bntx_tools.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import RandomizerCore.Tools.bntx_editor.bntx_editor as bntx_editor 3 | import RandomizerCore.Tools.oead_tools as oead_tools 4 | from RandomizerCore.Paths.randomizer_paths import RESOURCE_PATH, IS_RUNNING_FROM_SOURCE 5 | import os 6 | import struct 7 | import quicktex.dds as quicktex_dds 8 | import quicktex.s3tc.bc3 as bc3 9 | from io import BytesIO 10 | 11 | from RandomizerCore.Randomizers import data 12 | from RandomizerCore.Tools.bntx_editor import bfres 13 | 14 | 15 | # This method aims to create a custom BNTX archive based on the original one to add a custom title screen 16 | def createRandomizerTitleScreenArchive(sarc_data): 17 | editor = bntx_editor.BNTXEditor() 18 | editor.openFile(sarc_data.reader.get_file('timg/__Combined.bntx').data.tobytes()) 19 | 20 | texture_to_replace = 'Logo_00^f' 21 | logo_texs = [t for t in editor.bntx.textures if t.name == texture_to_replace] 22 | if logo_texs: 23 | texture_index = editor.bntx.textures.index(logo_texs[0]) 24 | else: 25 | raise Exception(f'Texture {texture_to_replace} not found') 26 | 27 | # Extracting the texture as DDS 28 | dds_tex = BytesIO(editor.exportTexByIndex(texture_index)) 29 | png_tex = BytesIO() 30 | new_png = BytesIO() 31 | new_dds = BytesIO() 32 | 33 | # Convert texture to PNG 34 | im = Image.open(dds_tex) 35 | im.save(png_tex, format="PNG") 36 | png_tex.seek(0) 37 | 38 | # Merge our PNG with the original one to create the new title screen 39 | background = Image.open(png_tex) 40 | foreground = Image.open(os.path.join(RESOURCE_PATH, 'randomizer.png')) 41 | background.paste(foreground, (0, 0), foreground) 42 | background.save(new_png, format="PNG") 43 | new_png.seek(0) 44 | 45 | updated_logo = Image.open(new_png) 46 | 47 | # Convert back to DDS using QuickTex 48 | save(quicktex_dds.encode(updated_logo, bc3.BC3Encoder(18), 'DXT5'), new_dds) 49 | new_dds.seek(0) 50 | 51 | # Inject it back to the BNTX File 52 | editor.replaceTexByIndex(new_dds, texture_index) 53 | sarc_data.writer.files['timg/__Combined.bntx'] = editor.save() 54 | 55 | 56 | def save(dds_file, new_dds: BytesIO): 57 | """rewrite of quicktex DDSFile save function to work with BytesIO""" 58 | 59 | new_dds.write(b'DDS ') 60 | 61 | # WRITE HEADER 62 | new_dds.write( 63 | struct.pack( 64 | '<7I44x', 65 | 124, 66 | int(dds_file.flags), 67 | dds_file.size[1], 68 | dds_file.size[0], 69 | dds_file.pitch, 70 | dds_file.depth, 71 | dds_file.mipmap_count, 72 | ) 73 | ) 74 | new_dds.write( 75 | struct.pack( 76 | '<2I4s5I', 77 | 32, 78 | int(dds_file.pf_flags), 79 | bytes(dds_file.four_cc, 'ascii'), 80 | dds_file.pixel_size, 81 | *dds_file.pixel_bitmasks, 82 | ) 83 | ) 84 | new_dds.write(struct.pack('<4I4x', *dds_file.caps)) 85 | 86 | assert new_dds.tell() == 128, 'error writing file: incorrect header size' 87 | 88 | for texture in dds_file.textures: 89 | new_dds.write(texture) 90 | 91 | 92 | def replaceTextureInFile(bntxFileInput, bntxFileOutput, textureName, textureFile): 93 | editor = bntx_editor.BNTXEditor() 94 | with open(bntxFileInput, 'rb') as bntxFileInputFile: 95 | bntxFileInputData = bntxFileInputFile.read() 96 | editor.openFile(bntxFileInputData) 97 | editor.replaceTextureByName(textureName, textureFile) 98 | editor.saveAs(bntxFileOutput) 99 | 100 | 101 | def createChestBfresWithCustomTexturesIfMissing(chestBfresPath, bfresOutputFolder): 102 | 103 | # Checking bfres folder path 104 | if not os.path.exists(bfresOutputFolder): 105 | os.makedirs(bfresOutputFolder) 106 | 107 | # Checking if we need to generate something 108 | textureTypes = ['Junk', 'Key', 'LifeUpgrade'] 109 | missingTextureTypes = [] 110 | 111 | for textureType in textureTypes: 112 | if IS_RUNNING_FROM_SOURCE: # always create the files when running from source to make sure there aren't any issues 113 | missingTextureTypes.append(textureType) 114 | continue 115 | 116 | if not os.path.exists(os.path.join(RESOURCE_PATH, 'textures', f'ObjTreasureBox{textureType}.bfres')): 117 | missingTextureTypes.append(textureType) 118 | 119 | if len(missingTextureTypes) == 0: 120 | return 121 | 122 | # If we have missing textures, then extracting needed files from RomFS and re-injecting them 123 | # Reading and extracting BNTX from original chest BFRES file 124 | chestBfres = bfres.File() 125 | chestBfres.readFromFile(chestBfresPath) 126 | 127 | # Extracting BNTX file 128 | chestBntxFilepath = os.path.join(RESOURCE_PATH, 'textures', 'chestTexture.bntx') 129 | chestBfres.extractMainBNTX(chestBntxFilepath) 130 | 131 | temporaryBntx = os.path.join(RESOURCE_PATH, 'textures', 'chestTextures_updated.bntx') 132 | 133 | for textureType in missingTextureTypes: 134 | texture_file = os.path.join( 135 | RESOURCE_PATH, 136 | 'textures', 137 | 'chest', 'TreasureBox' + textureType + '.dds' 138 | ) 139 | replaceTextureInFile( 140 | chestBntxFilepath, 141 | temporaryBntx, 142 | 'MI_dungeonTreasureBox_01_alb', 143 | texture_file 144 | ) 145 | 146 | # Injecting BNTX in the BFRES file 147 | chestBfres.replaceMainBNTX(temporaryBntx) 148 | chestBfres.saveAs(os.path.join(bfresOutputFolder, f'ObjTreasureBox{textureType}.bfres')) 149 | 150 | # Removing temporary file 151 | os.remove(temporaryBntx) 152 | 153 | # Removing extracted BNTX 154 | os.remove(chestBntxFilepath) 155 | 156 | # Checking everything is there 157 | fileCount = 0 158 | for path in os.listdir(bfresOutputFolder): 159 | if os.path.isfile(os.path.join(bfresOutputFolder, path)): 160 | fileCount += 1 161 | 162 | if fileCount != (len(data.CHEST_TEXTURES) - 1): 163 | raise Exception('Missing bfres files in the output folder') 164 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/event_tools.py: -------------------------------------------------------------------------------- 1 | import evfl 2 | 3 | idgen = evfl.util.IdGenerator() 4 | 5 | 6 | def invertList(l): 7 | """Converts a list into a dict of {value: index} pairs""" 8 | return {l[i]: i for i in range(len(l))} 9 | 10 | 11 | def readFlow(evflFile): 12 | """Reads the flow from the eventflow file and returns it""" 13 | 14 | flow = evfl.EventFlow() 15 | with open(evflFile, 'rb') as file: 16 | flow.read(file.read()) 17 | 18 | return flow 19 | 20 | 21 | def writeFlow(evflFile, flow): 22 | """Writes the given flow to a new eventflowfile""" 23 | with open(evflFile, 'wb') as modified_file: 24 | flow.write(modified_file) 25 | 26 | 27 | def findEvent(flowchart, name): 28 | """Finds and returns an event from a flowchart given a name as a string. Returns None if not found""" 29 | 30 | if name == None: 31 | return 32 | 33 | for event in flowchart.events: 34 | if event.name == name: 35 | return event 36 | 37 | return None 38 | 39 | 40 | def findEntryPoint(flowchart, name): 41 | """Finds and returns an entry point from a flowchart given a name as a string. Returns None if not found""" 42 | 43 | if name == None: 44 | return 45 | 46 | for ep in flowchart.entry_points: 47 | if ep.name == name: 48 | return ep 49 | 50 | return None 51 | 52 | 53 | def findActor(flowchart, name, sub_name=''): 54 | """Finds and returns an actor from a flowchart given a name and an optional sub_name as a string 55 | 56 | If the actor does not exist, it is created and then returned""" 57 | 58 | identifier = evfl.ActorIdentifier(name, sub_name) 59 | try: 60 | return flowchart.find_actor(identifier) 61 | except ValueError: 62 | act = evfl.Actor() 63 | act.identifier = evfl.ActorIdentifier(name, sub_name) 64 | flowchart.actors.append(act) 65 | return flowchart.find_actor(act.identifier) 66 | 67 | 68 | def addActorAction(actor, action): 69 | """Appends an action to the actor in the flowchart""" 70 | 71 | has_action = False 72 | for act in actor.actions: 73 | if act.v == action: 74 | has_action = True 75 | break 76 | if not has_action: 77 | actor.actions.append(evfl.common.StringHolder(action)) 78 | 79 | 80 | def addActorQuery(actor, query): 81 | """Appends a query to an actor in the flowchart""" 82 | 83 | has_query = False 84 | for que in actor.queries: 85 | if que.v == query: 86 | has_query = True 87 | break 88 | 89 | if not has_query: 90 | actor.queries.append(evfl.common.StringHolder(query)) 91 | 92 | 93 | def addEntryPoint(flowchart, name): 94 | """Appends an entry point to the flowchart""" 95 | flowchart.entry_points.append(evfl.entry_point.EntryPoint(name)) 96 | 97 | 98 | def insertEventAfter(flowchart, previous, new): 99 | """Change the previous event or entry point to have {new} be the next event 100 | {previous} is the name of the event/entry point, {new} is the name of the event to add 101 | Return True if any event or entry point was modified and False if not""" 102 | 103 | newEvent = findEvent(flowchart, new) 104 | 105 | prevEvent = findEvent(flowchart, previous) 106 | if prevEvent: 107 | prevEvent.data.nxt.v = newEvent 108 | prevEvent.data.nxt.set_index(invertList(flowchart.events)) 109 | 110 | return True 111 | 112 | entry_point = findEntryPoint(flowchart, previous) 113 | if entry_point: 114 | entry_point.main_event.v = newEvent 115 | entry_point.main_event.set_index(invertList(flowchart.events)) 116 | return True 117 | 118 | return False 119 | 120 | 121 | def setSwitchEventCase(flowchart, switch, case, new): 122 | """Changes the specified case in a switch event into the new event""" 123 | 124 | newEvent = findEvent(flowchart, new) 125 | 126 | switchEvent = findEvent(flowchart, switch) 127 | if switchEvent: 128 | switchEvent.data.cases[case].v = newEvent 129 | switchEvent.data.cases[case].set_index(invertList(flowchart.events)) 130 | 131 | return True 132 | 133 | return False 134 | 135 | 136 | def removeEventAfter(flowchart, eventName): 137 | """Removes the next event from the specified event, so that there is nothing after it in the flow""" 138 | 139 | event = findEvent(flowchart, eventName) 140 | if not event: 141 | print('Not an event!') 142 | return 143 | 144 | event.data.nxt.v = None 145 | event.data.nxt.set_index(invertList(flowchart.events)) 146 | 147 | 148 | def insertActionChain(flowchart, before, events): 149 | """Inserts a chain of action events after a specified event. Returns if there are no events in the chain""" 150 | 151 | if len(events) == 0: 152 | return 153 | 154 | insertEventAfter(flowchart, before, events[0]) 155 | 156 | for i in range(1, len(events)): 157 | insertEventAfter(flowchart, events[i-1], events[i]) 158 | 159 | 160 | def createActionChain(flowchart, before, eventDefs, after=None): 161 | """Create a series of action events in order after {before} and followed by {after} 162 | Return the name of the first event in the chain""" 163 | if len(eventDefs) == 0: 164 | return 165 | 166 | next = after if len(eventDefs) == 1 else None 167 | first = createActionEvent(flowchart, eventDefs[0][0], eventDefs[0][1], eventDefs[0][2], next) 168 | current = first 169 | insertEventAfter(flowchart, before, current) 170 | 171 | for i in range(1, len(eventDefs)): 172 | next = None if i != len(eventDefs)-1 else after 173 | before = current 174 | current = createActionEvent(flowchart, eventDefs[i][0], eventDefs[i][1], eventDefs[i][2], after) 175 | insertEventAfter(flowchart, before, current) 176 | 177 | return first 178 | 179 | 180 | def createProgressiveItemSwitch(flowchart, item1, item2, flag, before=None, after=None): 181 | """Create a switch event leading to getting one of two options depending on whether a flag was set beforehand""" 182 | 183 | item1GetSeqEvent = createActionEvent(flowchart, 'Link', 'GenericItemGetSequenceByKey', 184 | {'itemKey': item1, 'keepCarry': False, 'messageEntry': ''}, after) 185 | item1AddEvent = createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 186 | {'itemKey': item1, 'count': 1, 'index': -1, 'autoEquip': False}, item1GetSeqEvent) 187 | 188 | item2GetSeqEvent = createActionEvent(flowchart, 'Link', 'GenericItemGetSequenceByKey', 189 | {'itemKey': item2, 'keepCarry': False, 'messageEntry': ''}, after) 190 | item2AddEvent = createActionEvent(flowchart, 'Inventory', 'AddItemByKey', 191 | {'itemKey': item2, 'count': 1, 'index': -1, 'autoEquip': False}, item2GetSeqEvent) 192 | 193 | flagSetEvent = createActionEvent(flowchart, 'EventFlags', 'SetFlag', 194 | {'symbol': flag, 'value': True}, item1AddEvent) 195 | flagCheckEvent = createSwitchEvent(flowchart, 'EventFlags', 'CheckFlag', 196 | {'symbol': flag}, {0: flagSetEvent, 1: item2AddEvent}) 197 | 198 | insertEventAfter(flowchart, before, flagCheckEvent) 199 | 200 | return flagCheckEvent 201 | 202 | 203 | def createActionEvent(flowchart, actor, action, params, nextev=None): 204 | """Creates a new action event. {actor} and {action} should be strings, {params} should be a dict 205 | {nextev} is the name of the next event""" 206 | 207 | act = findActor(flowchart, actor) 208 | addActorAction(act, action) 209 | 210 | nextEvent = findEvent(flowchart, nextev) 211 | 212 | if '[' in actor: 213 | actor = actor.replace(']', '') 214 | names = actor.split('[') 215 | act = evfl.ActorIdentifier(names[0], names[1]) 216 | else: 217 | act = evfl.ActorIdentifier(actor) 218 | 219 | new = evfl.event.Event() 220 | new.data = evfl.event.ActionEvent() 221 | new.data.actor = evfl.util.make_rindex(flowchart.find_actor(act)) 222 | new.data.actor.set_index(invertList(flowchart.actors)) 223 | new.data.actor_action = evfl.util.make_rindex(new.data.actor.v.find_action(action)) 224 | new.data.actor_action.set_index(invertList(new.data.actor.v.actions)) 225 | new.data.params = evfl.container.Container() 226 | new.data.params.data = params 227 | 228 | flowchart.add_event(new, idgen) 229 | 230 | if nextEvent: 231 | new.data.nxt.v = nextEvent 232 | new.data.nxt.set_index(invertList(flowchart.events)) 233 | 234 | return new.name 235 | 236 | 237 | def createSwitchEvent(flowchart, actor, query, params, cases): 238 | """Creates a new switch event and adds it to the flowchart 239 | {actor} and {query} should be strings, {params} should be a dict, {cases} is a dict if {int: event name}""" 240 | 241 | act = findActor(flowchart, actor) 242 | addActorQuery(act, query) 243 | 244 | new = evfl.event.Event() 245 | new.data = evfl.event.SwitchEvent() 246 | new.data.actor = evfl.util.make_rindex(flowchart.find_actor(evfl.common.ActorIdentifier(actor))) 247 | new.data.actor.set_index(invertList(flowchart.actors)) 248 | new.data.actor_query = evfl.util.make_rindex(new.data.actor.v.find_query(query)) 249 | new.data.actor_query.set_index(invertList(new.data.actor.v.queries)) 250 | new.data.params = evfl.container.Container() 251 | 252 | if "value1" in params: 253 | params["value1"] = evfl.common.Argument(params["value1"]) 254 | new.data.params.data = params 255 | 256 | flowchart.add_event(new, idgen) 257 | 258 | caseEvents = {} 259 | for case in cases: 260 | ev = findEvent(flowchart, cases[case]) 261 | if ev: 262 | caseEvents[case] = evfl.util.make_rindex(ev) 263 | caseEvents[case].set_index(invertList(flowchart.events)) 264 | 265 | new.data.cases = caseEvents 266 | 267 | return new.name 268 | 269 | 270 | def createSubFlowEvent(flowchart, refChart, entryPoint, params, nextev=None): 271 | """Creates a new subflow event and insert it into the flow 272 | {nextev} is the name of the next event""" 273 | 274 | nextEvent = findEvent(flowchart, nextev) 275 | 276 | new = evfl.event.Event() 277 | new.data = evfl.event.SubFlowEvent() 278 | new.data.params = evfl.container.Container() 279 | new.data.params.data = params 280 | new.data.res_flowchart_name = refChart 281 | new.data.entry_point_name = entryPoint 282 | 283 | flowchart.add_event(new, idgen) 284 | 285 | if nextEvent: 286 | new.data.nxt.v = nextEvent 287 | new.data.nxt.set_index(invertList(flowchart.events)) 288 | 289 | return new.name 290 | 291 | 292 | def createForkEvent(flowchart, before, forks, nextev=None): 293 | """Creates a new fork event and inserts it into the flow""" 294 | 295 | new = evfl.event.Event() 296 | new.data = evfl.event.ForkEvent() 297 | 298 | joinEvent = createJoinEvent(flowchart, nextev) 299 | new.data.join = evfl.util.make_rindex(joinEvent) 300 | new.data.join.set_index(invertList(flowchart.events)) 301 | 302 | flowchart.add_event(new, idgen) 303 | 304 | if before != None: 305 | insertEventAfter(flowchart, before, new.name) 306 | 307 | forkEvents = [] 308 | for branch in forks: 309 | ev = findEvent(flowchart, branch) 310 | if ev: 311 | fork = evfl.util.make_rindex(ev) 312 | fork.set_index(invertList(flowchart.events)) 313 | forkEvents.append(fork) 314 | 315 | new.data.forks = forkEvents 316 | 317 | return new.name, joinEvent.name 318 | 319 | 320 | def createJoinEvent(flowchart, nextev=None): 321 | """creates a new join event and inserts it into the flow""" 322 | 323 | nextEvent = findEvent(flowchart, nextev) 324 | 325 | new = evfl.event.Event() 326 | new.data = evfl.event.JoinEvent() 327 | 328 | flowchart.add_event(new, idgen) 329 | 330 | if nextEvent: 331 | new.data.nxt.v = nextEvent 332 | new.data.nxt.set_index(invertList(flowchart.events)) 333 | 334 | return new 335 | 336 | 337 | def setForkEventFork(flowchart, forkevent, fork, new): 338 | """Changes the specified fork in the forkevent to be {new}""" 339 | 340 | newEvent = findEvent(flowchart, new) 341 | 342 | forkEvent = findEvent(flowchart, forkevent) 343 | if forkEvent: 344 | forkEvent.data.forks[forkEvent.data.forks.index(fork)].v = newEvent 345 | forkEvent.data.forks[forkEvent.data.forks.index(fork)].set_index(invertList(flowchart.events)) 346 | 347 | return True 348 | 349 | return False 350 | 351 | 352 | def addForkEventForks(flowchart, forkevent, forks): 353 | """Adds new forks to the forkevent""" 354 | 355 | forkEvent = findEvent(flowchart, forkevent) 356 | 357 | forkEvents = forkEvent.data.forks 358 | for branch in forks: 359 | ev = findEvent(flowchart, branch) 360 | if ev: 361 | fork = evfl.util.make_rindex(ev) 362 | fork.set_index(invertList(flowchart.events)) 363 | forkEvents.append(fork) 364 | 365 | forkEvent.data.forks = forkEvents 366 | 367 | 368 | def setEventSong(flowchart, event_name, song): 369 | findEvent(flowchart, event_name).data.params.data['label'] = song 370 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/exefs_editor/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alden Mo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/exefs_editor/patcher.py: -------------------------------------------------------------------------------- 1 | from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN 2 | 3 | 4 | class Patcher: 5 | def __init__(self): 6 | """Initializes a patcher object to convert and write patches""" 7 | 8 | self.nso_header_offset = 0x100 9 | self.ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) 10 | self.patches = [] 11 | 12 | 13 | def addPatch(self, address: int, instruction: str, comment=None): 14 | """Changes the ASM instruction at address 15 | 16 | Multi-line instructions do not change what address we write to afterwards""" 17 | 18 | instruction = self.ks.asm(instruction, as_bytes=True)[0] 19 | self.patches.append((address, instruction, comment)) 20 | 21 | 22 | def replaceString(self, address: int, new_string: str, comment=None): 23 | """Changes a string in the data section into new_string""" 24 | 25 | self.patches.append((address, new_string, comment)) 26 | 27 | 28 | def replaceShort(self, address: int, value: int, comment=None): 29 | """Changes a short in the data section into value""" 30 | 31 | instruction = value.to_bytes(1, 'little', signed=True) 32 | self.patches.append((address, instruction, comment)) 33 | 34 | 35 | def generateIPS32Patch(self): 36 | """Writes and outputs the IPS32 patch""" 37 | 38 | result = b'' 39 | result += bytearray('IPS32', 'ascii') 40 | 41 | for patch in self.patches: 42 | address = patch[0] + self.nso_header_offset 43 | instruction = patch[1] 44 | if isinstance(instruction, str): 45 | instruction = instruction.replace('"', '') 46 | instruction = bytes(instruction, 'utf-8') + b'\x00' # null-terminated 47 | 48 | result += address.to_bytes(4, 'big') 49 | result += len(instruction).to_bytes(2, 'big') 50 | result += instruction 51 | 52 | result += bytearray('EEOF', 'ascii') 53 | 54 | return result 55 | 56 | 57 | def generatePCHTXT(self, build_id: str): 58 | version = "1.0.0" if build_id.startswith("A") else "1.0.1" 59 | outText = f"@nsobid-{build_id}\n" 60 | outText += f"# Zelda: Link's Awakening [01006BB00C6F0000] {version}\n\n" 61 | 62 | if self.nso_header_offset != 0: 63 | outText += f"@flag offset_shift {'0x{:x}'.format(self.nso_header_offset)}\n" 64 | 65 | for patch in self.patches: 66 | address, instruction, comment = patch 67 | address = address.to_bytes(4, 'big') 68 | if isinstance(instruction, bytes): 69 | instruction = instruction.hex().upper() 70 | if len(comment) > 0: 71 | outText += f"\n{comment}\n" 72 | outText += "@enabled\n" 73 | outText += f"{address.hex().upper()} {instruction}\n" 74 | 75 | outBuffer = bytearray(outText, "ascii") 76 | return outBuffer 77 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/fixed_hash.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | def readBytes(bytes, start, length, endianness='little'): 4 | return int.from_bytes(bytes[start : start + length], endianness) 5 | 6 | 7 | def readFloat(bytes, start, length): 8 | return float(struct.unpack('> 2) + (h << 5)) & 0xFFFFFFFF 31 | i += 1 32 | return h 33 | 34 | 35 | 36 | class Entry: 37 | def __init__(self, nodeIndex, name, nextOffset, data): 38 | self.nodeIndex = nodeIndex 39 | self.name = name 40 | self.nextOffset = nextOffset 41 | self.data = data 42 | 43 | 44 | 45 | class FixedHash: 46 | def __init__(self, data, offset=0): 47 | self.magic = readBytes(data, offset + 0x0, 1) 48 | self.version = readBytes(data, offset + 0x1, 1) 49 | self.numBuckets = readBytes(data, offset + 0x2, 2) 50 | self.numNodes = readBytes(data, offset + 0x4, 2) 51 | self.x6 = readBytes(data, offset + 0x6, 2) 52 | 53 | self.buckets = [] 54 | for i in range(self.numBuckets): # there will be an extra one. I don't really know what this data means but we want to preserve it 55 | self.buckets.append(readBytes(data, offset + 0x8 + (i * 4), 4)) 56 | 57 | entriesOffset = ((offset + 0x8 + 4*(self.numBuckets+1) + 3) & -8) + 8 58 | numEntries = readBytes(data, entriesOffset - 8, 8) // 0x10 59 | 60 | entryOffsetsOffset = entriesOffset + (numEntries * 0x10) + 8 61 | 62 | dataSectionOffset = ((entryOffsetsOffset + (4 * numEntries) + 7) & -8) + 8 63 | 64 | namesSectionOffset = ((dataSectionOffset + readBytes(data, dataSectionOffset - 8, 8) + 3) & -4) + 4 65 | namesSize = readBytes(data, namesSectionOffset - 4, 4) 66 | self.namesSection = data[namesSectionOffset : namesSectionOffset + namesSize] 67 | 68 | self.entries = [] 69 | for i in range(numEntries): 70 | currentOffset = entriesOffset + (i * 0x10) 71 | 72 | nodeIndex = readBytes(data, currentOffset, 2) 73 | 74 | nextOffset = readBytes(data, currentOffset + 8, 4) 75 | 76 | if namesSize: 77 | name = readString(data, namesSectionOffset + readBytes(data, currentOffset + 2, 2)) 78 | else: 79 | name = b'' 80 | 81 | entryDataOffset = readBytes(data, currentOffset + 0xC, 4) 82 | 83 | if nodeIndex <= 0xFFED: 84 | entryData = FixedHash(data, dataSectionOffset + entryDataOffset) 85 | #print(data[dataSectionOffset + entryDataOffset : dataSectionOffset + entryDataOffset + 32]) 86 | #pass 87 | elif nodeIndex >= 0xFFF0: 88 | dataSize = readBytes(data, dataSectionOffset + entryDataOffset, 8) 89 | 90 | entryData = data[dataSectionOffset + entryDataOffset + 8 : dataSectionOffset + entryDataOffset + 8 + dataSize] 91 | else: 92 | raise ValueError('Invalid node index') 93 | 94 | self.entries.append(Entry(nodeIndex, name, nextOffset, entryData)) 95 | 96 | def toBinary(self, offset=0): 97 | # Returns a bytes object of the fixed hash in binary form 98 | intro = b'' 99 | 100 | intro += self.magic.to_bytes(1, 'little') 101 | intro += self.version.to_bytes(1, 'little') 102 | intro += self.numBuckets.to_bytes(2, 'little') 103 | intro += self.numNodes.to_bytes(2, 'little') 104 | intro += self.x6.to_bytes(2, 'little') 105 | 106 | for bucket in self.buckets: 107 | intro += bucket.to_bytes(4, 'little') 108 | 109 | entriesSect = (len(self.entries) * 0x10).to_bytes(8, 'little') 110 | entryOffsetsSect = (len(self.entries) * 0x4).to_bytes(8, 'little') 111 | dataSect = b'' 112 | 113 | for i in range(len(self.entries)): 114 | entry = self.entries[i] 115 | 116 | entriesSect += entry.nodeIndex.to_bytes(2, 'little') 117 | if self.namesSection.count(entry.name) and self.namesSection != b'': 118 | entriesSect += self.namesSection.index(entry.name + b'\x00').to_bytes(2, 'little') 119 | else: 120 | entriesSect += b'\x00\x00' 121 | entriesSect += hash_string(entry.name).to_bytes(4, 'little') 122 | entriesSect += entry.nextOffset.to_bytes(4, 'little') 123 | entriesSect += len(dataSect).to_bytes(4, 'little') 124 | 125 | entryOffsetsSect += (i * 0x10).to_bytes(4, 'little') 126 | 127 | if entry.nodeIndex <= 0xFFED: 128 | dataSect += entry.data.toBinary(len(dataSect)) 129 | elif entry.nodeIndex >= 0xFFF0: 130 | dataSect += len(entry.data).to_bytes(8, 'little') + entry.data 131 | 132 | dataSect += b'\x00\x00\x00\x00\x00\x00\x00' 133 | dataSect = dataSect[:len(dataSect) & -8] 134 | else: 135 | raise ValueError('Invalid node index') 136 | 137 | dataSect = len(dataSect).to_bytes(8, 'little') + dataSect 138 | 139 | result = b'' 140 | result += intro 141 | 142 | while (len(result) + offset) % 8 != 0: 143 | result += b'\x00' 144 | result += entriesSect 145 | 146 | while (len(result) + offset) % 8 != 0: 147 | result += b'\x00' 148 | result += entryOffsetsSect 149 | 150 | while (len(result) + offset) % 8 != 0: 151 | result += b'\x00' 152 | result += dataSect 153 | 154 | while (len(result) + offset) % 4 != 0: 155 | result += b'\x00' 156 | result += len(self.namesSection).to_bytes(4, 'little') 157 | result += self.namesSection 158 | 159 | return result 160 | -------------------------------------------------------------------------------- /RandomizerCore/Tools/lvb.py: -------------------------------------------------------------------------------- 1 | from RandomizerCore.Tools.fixed_hash import * 2 | import struct 3 | 4 | 5 | 6 | class Level: 7 | def __init__(self, data): 8 | self.fixed_hash = FixedHash(data) 9 | 10 | # self.nodes = [] 11 | # node_entry = [e for e in self.fixed_hash.entries if e.name == b'node'][0] 12 | # for entry in node_entry.data.entries: 13 | # self.nodes.append(Node(entry.data)) 14 | 15 | self.zones = [] 16 | zone_entry = [e for e in self.fixed_hash.entries if e.name == b'zone'][0] 17 | for entry in zone_entry.data.entries: 18 | self.zones.append(Zone(entry.data)) 19 | 20 | # self.player_starts = [] 21 | # start_entry = [e for e in self.fixed_hash.entries if e.name == b'tagPlayerStart'][0] 22 | # for entry in start_entry.data.entries: 23 | # self.player_starts.append(tagPlayerStart(entry.name, entry.data)) 24 | 25 | config_entry = [e for e in self.fixed_hash.entries if e.name == b'config'][0] 26 | self.config = Config(config_entry.data) 27 | 28 | 29 | def repack(self): 30 | # new_names = b'' 31 | 32 | for entry in self.fixed_hash.entries: 33 | if entry.name == b'zone': 34 | entry.data.entries = [] 35 | for zone in self.zones: 36 | entry.data.entries.append(Entry(0xFFF0, b'', 0xFFFFFFFF, zone.pack())) 37 | 38 | # if entry.name == b'tagPlayerStart': 39 | # entry.data.entries = [] 40 | # for start in self.player_starts: 41 | # entry.data.entries.append(Entry(0xFFF0, start.name, 0xFFFFFFFF, start.pack())) 42 | 43 | if entry.name == b'config': 44 | entry.data = self.config.pack() 45 | 46 | # new_names += entry.name + b'\x00' 47 | 48 | # self.fixed_hash.namesSection = new_names 49 | 50 | return self.fixed_hash.toBinary() 51 | 52 | 53 | 54 | # This is a FixedHash child. Seems to contain an entry for each room? Each entry holds 32 bytes of data 55 | # Not yet understood 56 | class Node: 57 | def __init__(self, data): 58 | pass 59 | 60 | 61 | def repack(self): 62 | pass 63 | 64 | 65 | 66 | # This is a FixedHash child. Each entry holds 64 bytes of data 67 | # Not yet understood 68 | class Area: 69 | def __init__(self, data): 70 | pass 71 | 72 | 73 | def repack(self): 74 | pass 75 | 76 | 77 | 78 | # This is a FixedHash child. It contains an entry for each room where a room is defined by the camera bounds 79 | # Field is an exception, where it seems to define regions instead 80 | # This likely defines some properties for each room 81 | # This defines the room ID, BGM, ambience, and even some sort of room type 82 | # Not fully understood 83 | class Zone: 84 | def __init__(self, data): 85 | self.room_ID = readBytes(data, 0x0, 4) 86 | self.unknown_1 = data[0x4:0x3C] 87 | self.bgm = readString(data, 0x3C, as_string=True) 88 | self.se_amb = readString(data, 0x5C, as_string=True) 89 | self.group_amb = readString(data, 0x7C, as_string=True) 90 | self.unknown_2 = data[0x9C:0xB0] 91 | self.room_type = readString(data, 0xB0, as_string=True) 92 | 93 | 94 | def pack(self): 95 | packed = b'' 96 | packed += self.room_ID.to_bytes(4, 'little') 97 | packed += self.unknown_1 98 | packed += bytes(self.bgm, 'utf-8') 99 | 100 | padding = b'' 101 | for i in range(32-len(self.bgm)): 102 | padding += b'\x00' 103 | 104 | packed += padding 105 | packed += bytes(self.se_amb, 'utf-8') 106 | 107 | padding = b'' 108 | for i in range(32-len(self.se_amb)): 109 | padding += b'\x00' 110 | 111 | packed += padding 112 | packed += bytes(self.group_amb, 'utf-8') 113 | 114 | padding = b'' 115 | for i in range(32-len(self.group_amb)): 116 | padding += b'\x00' 117 | 118 | packed += padding 119 | packed += self.unknown_2 120 | packed += bytes(self.room_type, 'utf-8') 121 | 122 | padding = b'' 123 | for i in range(32-len(self.room_type)): 124 | padding += b'\x00' 125 | 126 | packed += padding 127 | 128 | return packed 129 | 130 | 131 | 132 | # This is a FixedHash child 133 | # There is an entry for every tagPlayerStart actor, with the entry name matching the first actor parameter 134 | # Each entry holds 16 bytes of data, the first 12 being 3 floats, which are the coordinate points of each actor 135 | # Last 4 bytes are not yet understood, although they appear to be Y-rotation / 45.0 136 | # For the rotations that aren't divided evenly, there's a decent margin of error 137 | class tagPlayerStart: 138 | def __init__(self, name, data): 139 | self.name = name 140 | self.pos_x = readFloat(data, 0x0, 4) 141 | self.pos_y = readFloat(data, 0x4, 4) 142 | self.pos_z = readFloat(data, 0x8, 4) 143 | self.unknown = data[0xC:0x10] 144 | 145 | 146 | def pack(self): 147 | packed = b'' 148 | packed += struct.pack(' 0: 57 | if type(l[0]) == dict: 58 | new = oead.gsheet.StructArray() 59 | elif type(l[0]) == bool: 60 | new = oead.gsheet.BoolArray() 61 | elif type(l[0]) == int: 62 | new = oead.gsheet.IntArray() 63 | elif type(l[0]) == float: 64 | new = oead.gsheet.FloatArray() 65 | elif type(l[0]) == str: 66 | new = oead.gsheet.StringArray() 67 | for s in l: 68 | if type(s) == dict: 69 | d = dictToStruct(s) 70 | new.append(d) 71 | # elif type(s) == list: 72 | # for k in s: 73 | # new.append(listToSheetArray(k, s)) 74 | else: 75 | new = oead.gsheet.StructArray() 76 | 77 | return new 78 | 79 | 80 | 81 | ### ROOT FIELDS 82 | def readField(field): 83 | return { 84 | 'name': field.name, 85 | 'type_name': field.type_name, 86 | 'type': field.type, 87 | 'flags': field.flags, 88 | 'fields': field.fields 89 | } 90 | 91 | 92 | def createField(name, type_name, type, offset, flags=None): 93 | field = oead.gsheet.Field() 94 | 95 | field.name = name 96 | field.type_name = type_name 97 | field.type = type 98 | 99 | if flags: 100 | field.flags = flags 101 | 102 | if field.type is oead.gsheet.Field.Type.String: 103 | field.inline_size = 0x10 104 | 105 | if field.type is oead.gsheet.Field.Type.Int: 106 | field.inline_size = 0x4 107 | 108 | if field.type is oead.gsheet.Field.Type.Struct: 109 | field.inline_size = 0x10 110 | field.fields.append(createField('category', 'ConditionCategory', oead.gsheet.Field.Type.Int, 16, oead.gsheet.Field.Flag.IsEnum)) 111 | field.fields.append(createField('parameter', 'string', oead.gsheet.Field.Type.String, 16)) 112 | 113 | field.data_size = field.inline_size 114 | field.offset_in_value = offset 115 | # field.x11 = 0 116 | 117 | return field 118 | 119 | 120 | def createFieldCondition(category, parameter): 121 | conditions = oead.gsheet.StructArray() 122 | conditions.append({'category': category, 'parameter': parameter}) 123 | 124 | return conditions 125 | 126 | 127 | 128 | ### CONDITIONS GSHEET 129 | # Return and create an empty structure for an element in the Conditions datasheet 130 | # checks is a list of (category, paramter) tuples 131 | def createCondition(name, checks): 132 | condition = {'symbol': name, 'conditions': oead.gsheet.StructArray()} 133 | for category, parameter in checks: 134 | condition['conditions'].append({'category': category, 'parameter': parameter}) 135 | 136 | return condition 137 | 138 | 139 | 140 | ### NPC GSHEET 141 | # # creates a new value for the npc gsheet 142 | # def newNpcFromDict(npc): 143 | # new = { 144 | # 'symbol': npc['symbol'], 145 | # 'graphics': { 146 | 147 | # } 148 | # } 149 | # new['symbol'] = npc['symbol'] 150 | # new['graphics'][] 151 | 152 | 153 | # create npc behavior 154 | def createBehavior(type, datas=None): 155 | behavior = {'type': type, 'parameters': oead.gsheet.StringArray()} 156 | if datas: 157 | print('working!') 158 | for data in datas: 159 | behavior['parameters'].append(data) 160 | 161 | return behavior 162 | 163 | 164 | # create npc eventTriggers 165 | def createEventTrigger(condition, additionalConditions, entryPoint): 166 | trigger = {'condition': condition, 'additionalConditions': oead.gsheet.StructArray(), 'entryPoint': entryPoint} 167 | for category, parameter in additionalConditions: 168 | trigger['additionalConditions'].append({'category': category, 'parameter': parameter}) 169 | 170 | return trigger 171 | 172 | 173 | # create npc layoutConditions 174 | def createLayoutCondition(category, parameter, layoutId=-1): 175 | layout = oead.gsheet.StructArray() 176 | layout.append({'category': category, 'parameter': parameter, 'layoutID': layoutId}) 177 | 178 | 179 | 180 | class SARC: 181 | def __init__(self, sarcFile: str): 182 | with open(sarcFile, 'rb') as f: 183 | self.reader = oead.Sarc(f.read()) 184 | self.writer = oead.SarcWriter.from_sarc(self.reader) 185 | oead.SarcWriter.set_endianness(self.writer, oead.Endianness.Little) # Switch uses Little Endian 186 | 187 | def repack(self): 188 | return self.writer.write()[1] 189 | -------------------------------------------------------------------------------- /RandomizerCore/randomizer_data.py: -------------------------------------------------------------------------------- 1 | from RandomizerCore.Paths.randomizer_paths import DATA_PATH, RESOURCE_PATH, SETTINGS_PATH, LOGIC_PATH 2 | 3 | import yaml 4 | import os 5 | 6 | VERSION = 0.41 7 | 8 | DOWNLOAD_PAGE = 'https://github.com/Owen-Splat/LAS-Randomizer/releases/latest' 9 | 10 | with open(os.path.join(RESOURCE_PATH, 'light_theme.txt'), 'r') as f: 11 | LIGHT_STYLESHEET = f.read() 12 | 13 | with open(os.path.join(RESOURCE_PATH, 'dark_theme.txt'), 'r') as f: 14 | DARK_STYLESHEET = f.read() 15 | 16 | with open(os.path.join(RESOURCE_PATH, 'changes.txt'), 'r') as f: 17 | CHANGE_LOG = f.read() 18 | 19 | with open(os.path.join(RESOURCE_PATH, 'issues.txt'), 'r') as f: 20 | KNOWN_ISSUES = f.read() 21 | 22 | with open(os.path.join(RESOURCE_PATH, 'tips.txt'), 'r') as f: 23 | HELPFUL_TIPS = f.read() 24 | 25 | with open(os.path.join(RESOURCE_PATH, 'about.txt'), 'r') as f: 26 | ABOUT_INFO = f.read() 27 | 28 | with open(os.path.join(DATA_PATH, 'items.yml'), 'r') as f: 29 | items = yaml.safe_load(f) 30 | ITEM_DEFS = items['Item_Pool'] 31 | STARTING_ITEMS = list(items['Starting_Items']) 32 | 33 | with open(LOGIC_PATH, 'r') as f: 34 | LOGIC_VERSION = float(f.readline().strip('#')) 35 | LOGIC_RAW = f.read() 36 | # LOGIC_DEFS = yaml.safe_load(f) 37 | # TRICKS = [k for k, v in LOGIC_DEFS.items() if v['type'] == 'trick'] 38 | 39 | with open(os.path.join(DATA_PATH, 'enemies.yml'), 'r') as f: 40 | ENEMY_DATA = yaml.safe_load(f) 41 | 42 | with open(os.path.join(DATA_PATH, 'locations.yml'), 'r') as f: 43 | LOCATIONS = yaml.safe_load(f) 44 | 45 | with open(os.path.join(RESOURCE_PATH, 'adjectives.txt'), 'r') as f: 46 | ADJECTIVES = f.read().splitlines() 47 | 48 | with open(os.path.join(RESOURCE_PATH, 'characters.txt'), 'r') as f: 49 | CHARACTERS = f.read().splitlines() 50 | 51 | try: 52 | with open(SETTINGS_PATH, 'r') as settingsFile: 53 | SETTINGS = yaml.safe_load(settingsFile) 54 | DEFAULTS = False 55 | except FileNotFoundError: 56 | DEFAULTS = True 57 | SETTINGS = {} 58 | 59 | MISCELLANEOUS_CHESTS = LOCATIONS['Chest_Locations'] 60 | FISHING_REWARDS = LOCATIONS['Fishing_Rewards'] 61 | RAPIDS_REWARDS = LOCATIONS['Rapids_Rewards'] 62 | DAMPE_REWARDS = LOCATIONS['Dampe_Rewards'] 63 | # SHOP_ITEMS = LOCATIONS['Shop_Items'] 64 | # TRENDY_REWARDS = LOCATIONS['Trendy_Rewards'] 65 | FREE_GIFT_LOCATIONS = LOCATIONS['Free_Gifts'] 66 | TRADE_GIFT_LOCATIONS = LOCATIONS['Trade_Gifts'] 67 | BOSS_LOCATIONS = LOCATIONS['Boss_Locations'] 68 | MISC_LOCATIONS = LOCATIONS['Misc_Items'] 69 | SEASHELL_REWARDS = LOCATIONS['Mansion'] 70 | HEART_PIECE_LOCATIONS = LOCATIONS['Heart_Pieces'] 71 | LEAF_LOCATIONS = LOCATIONS['Golden_Leaves'] 72 | DUNGEON_OWLS = LOCATIONS['Dungeon_Owl_Statues'] 73 | OVERWORLD_OWLS = LOCATIONS['Overworld_Owl_Statues'] 74 | BLUE_RUPEES = LOCATIONS['Blue_Rupees'] 75 | 76 | TOTAL_CHECKS = set([ 77 | *MISCELLANEOUS_CHESTS, *FISHING_REWARDS, *RAPIDS_REWARDS, 78 | *DAMPE_REWARDS, *FREE_GIFT_LOCATIONS, *TRADE_GIFT_LOCATIONS, 79 | *BOSS_LOCATIONS, *MISC_LOCATIONS, *SEASHELL_REWARDS, 80 | *HEART_PIECE_LOCATIONS, *LEAF_LOCATIONS, *DUNGEON_OWLS, 81 | *OVERWORLD_OWLS, *BLUE_RUPEES, #*SHOP_ITEMS, *TRENDY_REWARDS 82 | ]) 83 | 84 | SEASHELL_VALUES = (0, 5, 15, 30, 40, 50) 85 | 86 | LOGIC_PRESETS = ('basic', 'advanced', 'glitched', 'hell', 'none') 87 | 88 | OWLS_SETTINGS = ('none', 'overworld', 'dungeons', 'all') # ('vanilla', 'hints', 'gifts', 'hybrid') 89 | 90 | TRAP_SETTINGS = ('none', 'few', 'several', 'many', 'trapsanity') 91 | 92 | CHEST_ASPECT_SETTINGS = ('default', 'csmc', 'camc') 93 | 94 | DUNGEON_ITEM_SETTINGS = ('none', 'stone-beak', 'mc', 'mcb') 95 | 96 | # KEYSANITY_SETTINGS = ('standard', 'keys', 'keys+mcb') 97 | 98 | PLATFORMS = ('console', 'emulator') 99 | 100 | STEALING_REQUIREMENTS = ('always', 'never', 'normal') 101 | -------------------------------------------------------------------------------- /RandomizerCore/spoiler.py: -------------------------------------------------------------------------------- 1 | from RandomizerCore.Paths.randomizer_paths import IS_RUNNING_FROM_SOURCE 2 | import os 3 | 4 | 5 | def generateSpoilerLog(placements, logic_defs, out_dir, seed): 6 | # Make the output directory if it doesnt exist 7 | if not os.path.exists(out_dir): 8 | os.makedirs(out_dir) 9 | 10 | regions = {'mabe-village': [], 'toronbo-shores': [], 'mysterious-woods': [], 'koholint-prairie': [], 'tabahl-wasteland': [], 'ukuku-prairie': [], 'sign-maze': [], 'goponga-swamp': [], 'taltal-heights': [], 'marthas-bay': [], 'kanalet-castle': [], 'pothole-field': [], 'animal-village': [], 'yarna-desert': [], 'ancient-ruins': [], 'rapids-ride': [], 'taltal-mountains-east': [], 'taltal-mountains-west': [], 'color-dungeon': [], 'tail-cave': [], 'bottle-grotto': [], 'key-cavern': [], 'angler-tunnel': [], 'catfish-maw': [], 'face-shrine': [], 'eagle-tower': [], 'turtle-rock': []} 11 | 12 | for key in logic_defs: 13 | if not key.startswith('starting-item') and logic_defs[key]['type'] in ['item', 'follower']: 14 | regions[logic_defs[key]['spoiler-region']].append(key) 15 | 16 | with open(f'{out_dir}/spoiler_{seed}.txt', 'w') as output: 17 | output.write('settings:\n') 18 | sets = list(placements['settings']) 19 | sets.sort() 20 | for setting in sets: 21 | if setting not in ('excluded-locations', 'starting-items'): 22 | output.write(f' {setting}: {placements["settings"][setting]}\n') 23 | 24 | output.write('\nstarting-items:\n') 25 | items = list(placements['starting-items']) 26 | items.sort() 27 | for item in items: 28 | output.write(f' {item}\n') 29 | 30 | output.write('\nexcluded-locations:\n') 31 | junk = list(placements['force-junk']) 32 | junk.sort() 33 | for location in junk: 34 | output.write(f' {location}\n') 35 | 36 | output.write('\ndungeon-entrances:\n') 37 | for dun in placements['dungeon-entrances']: 38 | output.write(f' {dun} -> {placements["dungeon-entrances"][dun]}\n') 39 | 40 | output.write('\n') 41 | for key in regions: 42 | output.write(f'{key}:\n') 43 | for location in regions[key]: 44 | item = placements[location] 45 | index = -1 46 | if location in placements['indexes']: 47 | index = placements['indexes'][location] 48 | index = f'[{index}]' if (IS_RUNNING_FROM_SOURCE and index > -1) else '' 49 | if location.startswith('starting-dungeon-item'): 50 | continue 51 | if item.endswith('trap'): 52 | item = 'trap' 53 | output.write(' {0}: {1}{2}\n'.format(location, item, index)) 54 | -------------------------------------------------------------------------------- /RandomizerUI/Resources/about.txt: -------------------------------------------------------------------------------- 1 | Created by Glan, Owen_Splat

2 |

3 | 4 | Report issues here:

5 | https://github.com/Owen-Splat/LAS-Randomizer/issues 6 |



7 | 8 | Join our Discord community!

9 | https://discord.com/invite/rfBSCUfzj8 -------------------------------------------------------------------------------- /RandomizerUI/Resources/adjectives.txt: -------------------------------------------------------------------------------- 1 | Adorable 2 | Adventurous 3 | Aggressive 4 | Angry 5 | Annoying 6 | Arrogant 7 | Attractive 8 | Audacious 9 | Awful 10 | Bad 11 | Beautiful 12 | Blushing 13 | Bored 14 | Brave 15 | Breakable 16 | Bright 17 | Busy 18 | Calm 19 | Careful 20 | Charming 21 | Cheerful 22 | Clean 23 | Clever 24 | Clumsy 25 | Colorful 26 | Combative 27 | Cool 28 | Crazy 29 | Creepy 30 | Cruel 31 | Cute 32 | Dangerous 33 | Defeated 34 | Defiant 35 | Depressed 36 | Distraught 37 | Disturbed 38 | Dizzy 39 | Eager 40 | Elegant 41 | Embarrassed 42 | Embiggened 43 | Evil 44 | Expensive 45 | Famous 46 | Fancy 47 | Fierce 48 | Foolish 49 | Frightened 50 | Gentle 51 | Good 52 | Graceful 53 | Grumpy 54 | Happy 55 | Healthy 56 | Helpless 57 | Homeless 58 | Horrible 59 | Hungry 60 | Innocent 61 | Jealous 62 | Jolly 63 | Kind 64 | Lazy 65 | Lonely 66 | Lucky 67 | Magnificent 68 | Maniacal 69 | Mysterious 70 | Naughty 71 | Nervous 72 | Nice 73 | Obnoxious 74 | Old 75 | Outrageous 76 | Poor 77 | Powerful 78 | Proud 79 | Puzzled 80 | Repulsive 81 | Rich 82 | Scared 83 | Scary 84 | Selfish 85 | Shy 86 | Strange 87 | Strong 88 | Talented 89 | Terrible 90 | Thankful 91 | Tired 92 | Troubled 93 | Unusual 94 | Upset 95 | Vicious 96 | Victorious 97 | Wandering 98 | Watchful 99 | Weak 100 | Weary 101 | Wicked 102 | Wild 103 | Young -------------------------------------------------------------------------------- /RandomizerUI/Resources/changes.txt: -------------------------------------------------------------------------------- 1 | New Features: 2 |



3 | - New Settings: One-Hit KO Mode, Nice Items, random enemy sizes, chest type matches content, and more! 4 |



5 | - Settings string: Share settings with others, or even randomize settings 6 |



7 | 8 | Changes: 9 |



10 | - Songs now display Marin/Manbo/Mamu when obtaining them 11 |



12 | - NPCs now hold up the proper model before giving the item 13 |



14 | - Reduced-Farming has been removed and is now baseline behavior 15 |



16 | - A single Bomb/Powder/Arrow now gives 3 of that item 17 |



18 | - Fast-Trendy has been removed and Trendy-Prize-Final is forced vanilla 19 |



20 | 21 | Bug Fixes: 22 |



23 | - Bombs & Powder can now be obtained properly from other sources 24 |



25 | - Fixed some compatibility issues across settings 26 |



27 | - Fixed some issues when specific items were already obtained 28 |



29 | - Tons of minor bug fixes to improve the gameplay experience -------------------------------------------------------------------------------- /RandomizerUI/Resources/characters.txt: -------------------------------------------------------------------------------- 1 | Armos 2 | Beamos 3 | Blaino 4 | Blob 5 | Blooper 6 | Boarblin 7 | Bombite 8 | Boo 9 | BowWow 10 | Bubble 11 | Crow 12 | Cucco 13 | Dampe 14 | Darknut 15 | DethI 16 | Dodongo 17 | Eagle 18 | Facade 19 | Fairy 20 | Fisherman 21 | Fox 22 | Gel 23 | Genie 24 | Gibdo 25 | Gohma 26 | Goomba 27 | Helmasaur 28 | Hinox 29 | HotHead 30 | Keese 31 | Kiki 32 | Kirby 33 | LikeLike 34 | Link 35 | MadBatter 36 | Mamu 37 | Manbo 38 | Marin 39 | Mermaid 40 | Mimic 41 | Moblin 42 | Moldorm 43 | Octorok 44 | Owl 45 | Pairodd 46 | Papahl 47 | Pincer 48 | Piranha 49 | Podoboo 50 | Pokey 51 | Rabbit 52 | Richard 53 | Rover 54 | ShyGuy 55 | Spark 56 | Specter 57 | Stalfos 58 | Tarin 59 | Tektite 60 | Thwomp 61 | Tracy 62 | Ulrira 63 | Vire 64 | Walrus 65 | WindFish 66 | Wizzrobe 67 | Zirro 68 | Zombie 69 | Zora -------------------------------------------------------------------------------- /RandomizerUI/Resources/dark_theme.txt: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021-2022 Yunosuke Ohsugi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | 26 | /* Modified from PyQtDarkTheme */ 27 | 28 | QWidget { 29 | background: rgb(30, 35, 40); 30 | color: rgb(200, 205, 210); 31 | selection-background-color: rgb(120, 180, 250); 32 | selection-color: rgb(30, 35, 40); 33 | } 34 | QWidget:disabled { 35 | color: rgb(105, 110, 115); 36 | selection-background-color: rgb(80, 85, 90); 37 | selection-color: rgb(105, 110, 115); 38 | } 39 | QMainWindow::separator { 40 | width: 4px; 41 | height: 4px; 42 | background: rgb(60, 65, 70); 43 | } 44 | QMainWindow::separator:hover, 45 | QMainWindow::separator:pressed { 46 | background: rgb(120, 180, 250); 47 | } 48 | QCheckBox { 49 | border: 1px solid transparent; 50 | spacing: 8px; 51 | } 52 | QCheckBox:hover { 53 | border-bottom: 2px solid rgb(120, 180, 250); 54 | } 55 | QCheckBox::indicator { 56 | height: 18px; 57 | width: 18px; 58 | } 59 | QMenuBar { 60 | background: rgb(30, 35, 40); 61 | padding: 2px; 62 | border-bottom: 1px solid rgb(60, 65, 70); 63 | } 64 | QMenuBar::item { 65 | background: transparent; 66 | padding: 4px; 67 | } 68 | QMenuBar::item:selected { 69 | padding: 4px; 70 | background: rgb(70, 75, 80); 71 | border-radius: 4px; 72 | } 73 | QMenuBar::item:pressed { 74 | padding: 4px; 75 | } 76 | QProgressBar { 77 | border: 1px solid rgb(60, 65, 70); 78 | text-align: center; 79 | border-radius: 4px; 80 | } 81 | QProgressBar::chunk { 82 | background: rgb(120, 180, 250); 83 | } 84 | QComboBox { 85 | border: 1px solid rgb(60, 65, 70); 86 | background: rgb(50, 55, 60); 87 | border-radius: 1px; 88 | } 89 | QComboBox:hover { 90 | border: 1px solid rgb(120, 180, 250); 91 | } 92 | QComboBox::drop-down { 93 | padding-right: 4px; 94 | } 95 | QComboBox::item:selected { 96 | border: none; 97 | background: rgb(0, 75, 125); 98 | color: rgb(200, 205, 210); 99 | } 100 | QComboBox::indicator:checked { 101 | width: 18px; 102 | } 103 | QLineEdit, 104 | QListWidget, 105 | QSpinBox { 106 | background: rgb(50, 55, 60); 107 | border: 1px solid rgb(60, 65, 70); 108 | } 109 | QLineEdit:hover{ 110 | border: 1px solid rgb(0, 75, 125); 111 | } 112 | QLineEdit:focus { 113 | border: 1px solid rgb(120, 180, 250); 114 | } 115 | QLineEdit:disabled { 116 | background: rgb(30, 35, 40); 117 | border: 1px solid rgb(40, 45, 50); 118 | } 119 | QTabWidget::pane { 120 | border: 1px solid rgb(60, 65, 70); 121 | border-radius: 4px; 122 | } 123 | QTabBar { 124 | qproperty-drawBase: 0; 125 | } 126 | QTabBar::tab { 127 | padding: 3px; 128 | } 129 | QTabBar::tab:hover { 130 | background: rgba(40, 70, 100, 0.333); 131 | } 132 | QTabBar::tab:selected { 133 | background: rgba(40, 70, 100, 0.933); 134 | } 135 | QTabBar::tab:top { 136 | border-bottom: 2px solid rgb(60, 65, 70); 137 | margin-left: 4px; 138 | border-top-left-radius: 2px; 139 | border-top-right-radius: 2px; 140 | } 141 | QTabBar::tab:top:selected { 142 | border-bottom: 2px solid rgb(120, 180, 250); 143 | } 144 | QTabBar::tab:top:hover { 145 | border-color: rgb(120, 180, 250); 146 | } 147 | QPushButton { 148 | background-color: rgb(200, 205, 210); 149 | border: 1px solid rgb(60, 65, 70); 150 | color: black; 151 | min-width: 75px; 152 | min-height: 23px; 153 | font-size:calc(12px + 1.5vw); /* cool hack to set minimum font size */ 154 | } 155 | QPushButton:hover { 156 | border-color: rgb(120, 180, 250); 157 | } 158 | QScrollBar:vertical { 159 | background-color: rgb(40, 45, 50); 160 | width: 15px; 161 | margin: 15px 3px 15px 3px; 162 | border: 1px transparent rgb(40, 45, 50); 163 | border-radius: 2px; 164 | } 165 | QScrollBar::handle:vertical { 166 | background-color: rgba(120, 180, 250, 0.333); 167 | border-radius: 2px; 168 | } 169 | QScrollBar::sub-line:vertical { 170 | margin: 3px 0px 3px 0px; 171 | height: 12px; 172 | width: 15px; 173 | subcontrol-position: top; 174 | subcontrol-origin: margin; 175 | } 176 | QScrollBar::add-line:vertical { 177 | margin: 3px 0px 3px 0px; 178 | height: 12px; 179 | width: 15px; 180 | subcontrol-position: bottom; 181 | subcontrol-origin: margin; 182 | } 183 | QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { 184 | background: none; 185 | } 186 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 187 | background: none; 188 | } 189 | QGroupBox { 190 | border: 2px solid rgb(50, 55, 60); 191 | border-radius: 5px; 192 | font-size: 12px; 193 | font-weight: bold; 194 | margin-top: 1ex; 195 | } 196 | QGroupBox::title { 197 | padding: 0 3px; 198 | subcontrol-origin: margin; 199 | subcontrol-position: top center; 200 | } 201 | QSpinBox:hover { 202 | border: 1px solid rgb(120, 180, 250); 203 | } -------------------------------------------------------------------------------- /RandomizerUI/Resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/3a1feb851255edf6fca56c586662232f0f6ed825/RandomizerUI/Resources/icon.icns -------------------------------------------------------------------------------- /RandomizerUI/Resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/3a1feb851255edf6fca56c586662232f0f6ed825/RandomizerUI/Resources/icon.ico -------------------------------------------------------------------------------- /RandomizerUI/Resources/issues.txt: -------------------------------------------------------------------------------- 1 | Issues: 2 |



3 | - Small key drops will trigger the compass ringtone regardless of what they are 4 |



5 | - While Trading Quest items work, they will not be displayed in the inventory 6 |



7 | - Dampé lacks logic for instrument shuffle 8 |



9 | - Falling down the TalTal East Drop without Flippers can result in a softlock -------------------------------------------------------------------------------- /RandomizerUI/Resources/light_theme.txt: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021-2022 Yunosuke Ohsugi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | 26 | /* Modified from PyQtDarkTheme */ 27 | 28 | QMainWindow::separator { 29 | width: 4px; 30 | height: 4px; 31 | background: rgb(60, 65, 70); 32 | } 33 | QMainWindow::separator:hover, 34 | QMainWindow::separator:pressed { 35 | background: rgb(120, 180, 250); 36 | } 37 | QCheckBox { 38 | border: 1px solid transparent; 39 | spacing: 8px; 40 | } 41 | QCheckBox:hover { 42 | border-bottom: 2px solid rgb(120, 180, 250); 43 | } 44 | QCheckBox::indicator { 45 | height: 18px; 46 | width: 18px; 47 | } 48 | QMenuBar { 49 | padding: 2px; 50 | border-bottom: 1px solid rgb(190, 195, 200); 51 | } 52 | QMenuBar::item { 53 | background: transparent; 54 | padding: 4px; 55 | } 56 | QMenuBar::item:selected { 57 | padding: 4px; 58 | background: rgba(40, 70, 100, 0.15); 59 | border-radius: 4px; 60 | } 61 | QMenuBar::item:pressed { 62 | padding: 4px; 63 | } 64 | QProgressBar { 65 | border: 1px solid rgb(190, 195, 200); 66 | text-align: center; 67 | border-radius: 4px; 68 | } 69 | QProgressBar::chunk { 70 | background: rgb(120, 180, 250); 71 | } 72 | QComboBox { 73 | border: 1px solid rgb(50, 55, 60); 74 | padding: 0 4px; 75 | border-radius: 1px; 76 | } 77 | QComboBox:hover { 78 | border: 1px solid rgb(120, 180, 250); 79 | } 80 | QComboBox::drop-down { 81 | padding-right: 4px; 82 | } 83 | QComboBox::item:selected { 84 | border: none; 85 | background: rgb(0, 75, 125); 86 | color: white; 87 | } 88 | QComboBox::indicator:checked { 89 | width: 18px; 90 | } 91 | QLineEdit:hover { 92 | border: 1px solid rgb(120, 180, 250); 93 | } 94 | QTabWidget::pane { 95 | border: 1px solid rgb(190, 195, 200); 96 | border-radius: 4px; 97 | } 98 | QTabBar { 99 | qproperty-drawBase: 0; 100 | } 101 | QTabBar::tab { 102 | padding: 3px; 103 | } 104 | QTabBar::tab:hover { 105 | background: rgba(40, 70, 100, 0.1); 106 | } 107 | QTabBar::tab:selected { 108 | background: rgba(40, 70, 100, 0.15); 109 | } 110 | QTabBar::tab:top { 111 | border-bottom: 2px solid rgb(120, 180, 250); 112 | margin-left: 4px; 113 | border-top-left-radius: 2px; 114 | border-top-right-radius: 2px; 115 | } 116 | QTabBar::tab:top:selected { 117 | border-bottom: 2px solid rgb(0, 75, 125); 118 | } 119 | QTabBar::tab:top:hover { 120 | border-color: rgb(0, 75, 125); 121 | } 122 | QPushButton { 123 | background-color: rgb(200, 205, 210); 124 | border: 1px solid rgb(60, 65, 70); 125 | color: black; 126 | min-width: 75px; 127 | min-height: 23px; 128 | font-size:calc(12px + 1.5vw); /* cool hack to set minimum font size */ 129 | } 130 | QPushButton:hover { 131 | border-color: rgb(120, 180, 250); 132 | } 133 | QPushButton:disabled { 134 | color: rgb(70, 75, 80); 135 | } 136 | QScrollBar:vertical { 137 | background-color: rgb(200, 205, 210); 138 | width: 15px; 139 | margin: 15px 3px 15px 3px; 140 | border: 1px solid rgb(190, 195, 200); 141 | border-radius: 2px; 142 | } 143 | QScrollBar::handle:vertical { 144 | background-color: rgba(0, 75, 125, 0.333); 145 | border-radius: 2px; 146 | } 147 | QScrollBar::sub-line:vertical { 148 | margin: 3px 0px 3px 0px; 149 | height: 12px; 150 | width: 15px; 151 | subcontrol-position: top; 152 | subcontrol-origin: margin; 153 | } 154 | QScrollBar::add-line:vertical { 155 | margin: 3px 0px 3px 0px; 156 | height: 12px; 157 | width: 15px; 158 | subcontrol-position: bottom; 159 | subcontrol-origin: margin; 160 | } 161 | QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { 162 | background: none; 163 | } 164 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 165 | background: none; 166 | } 167 | QGroupBox { 168 | border: 2px solid rgb(190, 195, 200); 169 | border-radius: 5px; 170 | font-size: 12px; 171 | font-weight: bold; 172 | margin-top: 1ex; 173 | } 174 | QGroupBox::title { 175 | padding: 0 3px; 176 | subcontrol-origin: margin; 177 | subcontrol-position: top center; 178 | } 179 | QMessageBox { 180 | background-color: rgb(240,240,240); 181 | } -------------------------------------------------------------------------------- /RandomizerUI/Resources/randomizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/3a1feb851255edf6fca56c586662232f0f6ed825/RandomizerUI/Resources/randomizer.png -------------------------------------------------------------------------------- /RandomizerUI/Resources/textures/chest/TreasureBoxJunk.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/3a1feb851255edf6fca56c586662232f0f6ed825/RandomizerUI/Resources/textures/chest/TreasureBoxJunk.dds -------------------------------------------------------------------------------- /RandomizerUI/Resources/textures/chest/TreasureBoxKey.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/3a1feb851255edf6fca56c586662232f0f6ed825/RandomizerUI/Resources/textures/chest/TreasureBoxKey.dds -------------------------------------------------------------------------------- /RandomizerUI/Resources/textures/chest/TreasureBoxLifeUpgrade.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/3a1feb851255edf6fca56c586662232f0f6ed825/RandomizerUI/Resources/textures/chest/TreasureBoxLifeUpgrade.dds -------------------------------------------------------------------------------- /RandomizerUI/Resources/tips.txt: -------------------------------------------------------------------------------- 1 | - If you somehow get softlocked, dying and selecting "Save And Quit" will spawn you at Marin's house. 2 | 3 | - This game autosaves each time the player obtains an item. Reload that autosave to get back to the beginning of a dungeon! 4 | 5 | - You can switch tunics in telephone booths! 6 | 7 | - The ghost grave check requires the Surf Harp, you simply just need to walk up to the grave to obtain the item 8 | 9 | - The pothole final check currently requires talking to the owl statue when you have the shovel -------------------------------------------------------------------------------- /RandomizerUI/UI/ui_progress_form.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ui_progress_form.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.3.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QProgressBar, 19 | QPushButton, QSizePolicy, QWidget) 20 | 21 | class Ui_ProgressWindow(object): 22 | def setupUi(self, ProgressWindow): 23 | if not ProgressWindow.objectName(): 24 | ProgressWindow.setObjectName(u"ProgressWindow") 25 | ProgressWindow.setWindowModality(Qt.ApplicationModal) 26 | ProgressWindow.resize(472, 125) 27 | ProgressWindow.setMinimumSize(QSize(472, 125)) 28 | self.centralwidget = QWidget(ProgressWindow) 29 | self.centralwidget.setObjectName(u"centralwidget") 30 | self.progressBar = QProgressBar(self.centralwidget) 31 | self.progressBar.setObjectName(u"progressBar") 32 | self.progressBar.setGeometry(QRect(10, 82, 451, 31)) 33 | self.progressBar.setValue(0) 34 | self.progressBar.setAlignment(Qt.AlignCenter) 35 | self.progressBar.setTextVisible(False) 36 | self.label = QLabel(self.centralwidget) 37 | self.label.setObjectName(u"label") 38 | self.label.setGeometry(QRect(10, 12, 451, 61)) 39 | self.label.setAlignment(Qt.AlignCenter) 40 | self.openOutputFolder = QPushButton(self.centralwidget) 41 | self.openOutputFolder.setObjectName(u"openOutputFolder") 42 | self.openOutputFolder.setEnabled(True) 43 | self.openOutputFolder.setGeometry(QRect(180, 85, 121, 24)) 44 | self.openOutputFolder.setCheckable(False) 45 | ProgressWindow.setCentralWidget(self.centralwidget) 46 | 47 | self.retranslateUi(ProgressWindow) 48 | 49 | QMetaObject.connectSlotsByName(ProgressWindow) 50 | # setupUi 51 | 52 | def retranslateUi(self, ProgressWindow): 53 | ProgressWindow.setWindowTitle(QCoreApplication.translate("ProgressWindow", u"ProgressWindow", None)) 54 | self.progressBar.setFormat("") 55 | self.label.setText(QCoreApplication.translate("ProgressWindow", u"Getting ready...", None)) 56 | self.openOutputFolder.setText(QCoreApplication.translate("ProgressWindow", u"Open output folder", None)) 57 | # retranslateUi 58 | 59 | -------------------------------------------------------------------------------- /RandomizerUI/UI/ui_progress_form.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProgressWindow 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 472 13 | 125 14 | 15 | 16 | 17 | 18 | 472 19 | 125 20 | 21 | 22 | 23 | ProgressWindow 24 | 25 | 26 | 27 | 28 | 29 | 10 30 | 82 31 | 451 32 | 31 33 | 34 | 35 | 36 | 0 37 | 38 | 39 | Qt::AlignCenter 40 | 41 | 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 10 52 | 12 53 | 451 54 | 61 55 | 56 | 57 | 58 | Getting ready... 59 | 60 | 61 | Qt::AlignCenter 62 | 63 | 64 | 65 | 66 | true 67 | 68 | 69 | 70 | 180 71 | 85 72 | 121 73 | 24 74 | 75 | 76 | 77 | Open output folder 78 | 79 | 80 | false 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /RandomizerUI/progress_window.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import subprocess 3 | from pathlib import Path 4 | 5 | from PySide6 import QtWidgets 6 | from RandomizerUI.UI.ui_progress_form import Ui_ProgressWindow 7 | from RandomizerCore.shuffler import ItemShuffler 8 | from RandomizerCore.mod_generator import ModsProcess 9 | import os 10 | import copy 11 | import shutil 12 | 13 | 14 | 15 | class ProgressWindow(QtWidgets.QMainWindow): 16 | 17 | def __init__(self, rom_path, out_dir, item_defs, logic_defs, settings, settings_string): 18 | super (ProgressWindow, self).__init__() 19 | self.ui = Ui_ProgressWindow() 20 | self.ui.setupUi(self) 21 | 22 | self.rom_path : str = rom_path 23 | self.out_dir : str = out_dir 24 | self.seed : str = settings['seed'] 25 | self.randstate = None 26 | self.logic : str = settings['logic'] 27 | self.item_defs = copy.deepcopy(item_defs) 28 | self.logic_defs = copy.deepcopy(logic_defs) 29 | self.settings = copy.deepcopy(settings) 30 | self.settings_string : str = settings_string 31 | 32 | self.num_of_mod_tasks = 255 33 | 34 | self.ui.openOutputFolder.setVisible(False) 35 | self.ui.openOutputFolder.clicked.connect(self.openOutputFolderButtonClicked) 36 | 37 | # if not settings['shuffle-companions']: 38 | # self.num_of_mod_files += 8 39 | 40 | if settings['blupsanity']: 41 | self.num_of_mod_tasks += 1 42 | 43 | if settings['owl-dungeon-gifts']: 44 | self.num_of_mod_tasks += 4 # 4 extra room modifications 45 | 46 | if settings['randomize-music']: 47 | self.num_of_mod_tasks += (102 + 13) # all .lvb files + extra events 48 | 49 | if settings['bad-pets']: 50 | self.num_of_mod_tasks += 10 51 | 52 | modded_enemies = 0 53 | if settings['randomize-enemies']: 54 | modded_enemies = 313 55 | if settings['randomize-enemy-sizes']: 56 | modded_enemies = 323 57 | self.num_of_mod_tasks += modded_enemies 58 | 59 | if settings['shuffle-dungeons']: 60 | self.num_of_mod_tasks += 19 61 | 62 | if settings['classic-d2']: 63 | self.num_of_mod_tasks += 1 64 | 65 | if settings['open-mabe']: 66 | self.num_of_mod_tasks += 4 67 | 68 | if settings['chest-aspect'] == 'camc': 69 | self.num_of_mod_tasks += 65 # len(PANEL_CHEST_ROOMS) 70 | 71 | self.done = False 72 | self.cancel = False 73 | 74 | self.shuffle_error = False 75 | self.mods_error = False 76 | 77 | self.shuffler_done = False 78 | self.mods_done = False 79 | 80 | self.placements = {} 81 | 82 | if os.path.exists(self.out_dir): # remove old mod files if generating a new one with the same seed 83 | shutil.rmtree(self.out_dir, ignore_errors=True) 84 | 85 | # initialize the shuffler thread 86 | self.current_job = 'shuffler' 87 | self.ui.progressBar.setMaximum(0) # busy status instead of direct progress 88 | self.ui.label.setText(f'Shuffling item placements...') 89 | self.shuffler_process =\ 90 | ItemShuffler(self.out_dir, self.seed, self.logic, self.settings, self.item_defs, self.logic_defs) 91 | self.shuffler_process.setParent(self) 92 | self.shuffler_process.give_placements.connect(self.receivePlacements) 93 | self.shuffler_process.is_done.connect(self.shufflerDone) 94 | self.shuffler_process.error.connect(self.shufflerError) 95 | self.shuffler_process.start() # start the item shuffler 96 | 97 | 98 | # receives the int signal as a parameter named progress 99 | def updateProgress(self, progress): 100 | self.ui.progressBar.setValue(progress) 101 | 102 | 103 | # receive the placements from the shuffler thread to the modgenerator 104 | def receivePlacements(self, placements): 105 | self.placements = placements[0] 106 | self.randstate = placements[1] 107 | 108 | 109 | def shufflerError(self, er_message=str): 110 | self.shuffle_error = True 111 | from RandomizerCore.Paths.randomizer_paths import LOGS_PATH 112 | with open(LOGS_PATH, 'w') as f: 113 | f.write(f'{self.seed} - {self.logic.capitalize()} Logic') 114 | f.write(f'\n{self.settings_string}') 115 | f.write(f'\n\n{er_message}') 116 | f.write(f'\n\n{self.settings}') 117 | 118 | 119 | # receive signals when threads are done 120 | def shufflerDone(self): 121 | if self.shuffle_error: 122 | self.ui.label.setText("Something went wrong! Please report this to either GitHub or Discord!") 123 | self.done = True 124 | return 125 | 126 | if self.cancel: 127 | self.done = True 128 | self.close() 129 | return 130 | 131 | # initialize the modgenerator thread 132 | self.current_job = 'modgenerator' 133 | self.ui.progressBar.setValue(0) 134 | self.ui.progressBar.setMaximum(self.num_of_mod_tasks) 135 | self.ui.progressBar.setTextVisible(True) 136 | self.ui.progressBar.setFormat("%p%") 137 | self.ui.label.setText(f'Generating mod files...') 138 | self.mods_process = ModsProcess(self.placements, self.rom_path, f'{self.out_dir}', self.item_defs, self.seed, self.randstate) 139 | self.mods_process.setParent(self) 140 | self.mods_process.progress_update.connect(self.updateProgress) 141 | self.mods_process.is_done.connect(self.modsDone) 142 | self.mods_process.error.connect(self.modsError) 143 | self.mods_process.start() # start the modgenerator 144 | 145 | 146 | def modsError(self, er_message=str): 147 | self.mods_error = True 148 | from RandomizerCore.Paths.randomizer_paths import LOGS_PATH 149 | with open(LOGS_PATH, 'w') as f: 150 | f.write(f"{self.seed} - {self.logic.capitalize()} Logic") 151 | f.write(f'\n{self.settings_string}') 152 | f.write(f"\n\n{er_message}") 153 | f.write(f"\n\n{self.settings}") 154 | 155 | 156 | def modsDone(self): 157 | if self.mods_error: 158 | self.ui.label.setText("Error detected! Please check that your romfs are valid!") 159 | if os.path.exists(self.out_dir): # delete files if user canceled 160 | shutil.rmtree(self.out_dir, ignore_errors=True) 161 | self.done = True 162 | return 163 | 164 | if self.cancel: 165 | self.ui.label.setText("Canceling...") 166 | if os.path.exists(self.out_dir): # delete files if user canceled 167 | shutil.rmtree(self.out_dir, ignore_errors=True) 168 | self.done = True 169 | self.close() 170 | return 171 | 172 | self.ui.progressBar.setValue(self.num_of_mod_tasks) 173 | self.ui.label.setText("All done! Check the README for instructions on how to play!") 174 | self.ui.progressBar.setVisible(False) 175 | self.ui.openOutputFolder.setVisible(True) 176 | self.done = True 177 | 178 | 179 | # override the window close event to close the randomization thread 180 | def closeEvent(self, event): 181 | if self.done: 182 | event.accept() 183 | else: 184 | event.ignore() 185 | self.cancel = True 186 | self.ui.label.setText('Canceling...') 187 | if self.current_job == 'shuffler': 188 | self.shuffler_process.stop() 189 | elif self.current_job == 'modgenerator': 190 | self.mods_process.stop() 191 | 192 | def openFolder(self, path): 193 | if platform.system() == "Windows": 194 | os.startfile(path) 195 | elif platform.system() == "Darwin": 196 | subprocess.Popen(["open", path]) 197 | else: 198 | subprocess.Popen(["xdg-open", path]) 199 | 200 | def openOutputFolderButtonClicked(self): 201 | self.openFolder(Path(self.out_dir).parent.absolute()) 202 | self.window().close() -------------------------------------------------------------------------------- /RandomizerUI/update.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtCore 2 | import urllib.request as lib 3 | 4 | from RandomizerCore.randomizer_data import VERSION, LOGIC_VERSION 5 | 6 | 7 | 8 | class UpdateProcess(QtCore.QThread): 9 | can_update = QtCore.Signal(bool) 10 | give_version = QtCore.Signal(float) 11 | 12 | 13 | def __init__(self, parent=None): 14 | QtCore.QThread.__init__(self, parent) 15 | 16 | 17 | def run(self): 18 | try: 19 | update_file =\ 20 | lib.urlopen("https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/master/version.txt") 21 | web_version = float(update_file.read().strip()) 22 | 23 | if web_version > VERSION: 24 | self.give_version.emit(web_version) 25 | self.can_update.emit(True) 26 | else: 27 | self.can_update.emit(False) 28 | 29 | except Exception: 30 | self.can_update.emit(False) 31 | 32 | 33 | 34 | class LogicUpdateProcess(QtCore.QThread): 35 | can_update = QtCore.Signal(bool) 36 | give_logic = QtCore.Signal(tuple) 37 | 38 | 39 | def __init__(self, parent=None, ver=LOGIC_VERSION): 40 | QtCore.QThread.__init__(self, parent) 41 | self.ver = ver 42 | 43 | 44 | def run(self): 45 | try: 46 | update_file =\ 47 | lib.urlopen("https://raw.githubusercontent.com/Owen-Splat/LAS-Randomizer/master/RandomizerCore/Data/logic.yml") 48 | web_version = float(update_file.readline().decode('utf-8').strip('#')) 49 | new_logic = update_file.read().decode('utf-8') 50 | 51 | if web_version > self.ver: 52 | self.give_logic.emit((web_version, new_logic)) 53 | self.can_update.emit(True) 54 | else: 55 | self.can_update.emit(False) 56 | 57 | except Exception: 58 | self.can_update.emit(False) 59 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | py -3.8 setup.py build 2 | if %errorlevel% neq 0 exit /b %errorlevel% 3 | py -3.8 build.py 4 | if %errorlevel% neq 0 exit /b %errorlevel% -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | from RandomizerCore.randomizer_data import VERSION 5 | 6 | import glob 7 | import sys 8 | 9 | base_name = f"LAS Randomizer v{VERSION}" 10 | build_path = os.path.join(".", "build") 11 | 12 | freeze_path_search = glob.glob(os.path.join(build_path, f"exe.*-{sys.version_info.major}.{sys.version_info.minor}")) 13 | if len(freeze_path_search) != 1: 14 | raise Exception('Freeze Path folder could not be identified.') 15 | 16 | freeze_path = freeze_path_search.pop() 17 | 18 | # Getting platform from folder name 19 | platform_re = re.search(r"exe\.(.*)-.*-.*[0-9]\.[0-9]", freeze_path) 20 | destination_platform = platform_re.group(1) 21 | 22 | base_name = f"LAS Randomizer v{VERSION} {destination_platform}" 23 | 24 | release_path = os.path.join(build_path, base_name) 25 | os.rename(freeze_path, release_path) 26 | shutil.copyfile("README.md", os.path.join(release_path, "README.txt")) 27 | shutil.copyfile("LICENSE.txt", os.path.join(release_path, "LICENSE.txt")) 28 | shutil.make_archive(release_path, "zip", release_path) 29 | -------------------------------------------------------------------------------- /randomizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from PySide6 import QtCore, QtGui, QtWidgets 4 | import RandomizerUI.main_window as window 5 | from RandomizerCore.Paths.randomizer_paths import RESOURCE_PATH, IS_RUNNING_FROM_SOURCE 6 | 7 | import os 8 | import sys 9 | 10 | def interruptHandler(sig, frame): 11 | sys.exit(0) 12 | 13 | # Allow keyboard interrupts 14 | import signal 15 | signal.signal(signal.SIGINT, interruptHandler) 16 | 17 | # Set app id so the custom taskbar icon will show while running from source 18 | if IS_RUNNING_FROM_SOURCE: 19 | try: 20 | from ctypes import windll 21 | windll.shell32.SetCurrentProcessExplicitAppUserModelID("Link's_Awakening_Switch_Randomizer") 22 | except AttributeError: 23 | pass # Ignore for versions of Windows before Windows 7 24 | except ImportError: 25 | if sys.platform != 'linux': raise 26 | 27 | build_icon = "icon.ico" 28 | if sys.platform == "darwin": # mac 29 | build_icon = "icon.icns" 30 | 31 | app = QtWidgets.QApplication([]) 32 | app.setStyle('cleanlooks') 33 | app.setWindowIcon(QtGui.QIcon(os.path.join(RESOURCE_PATH, build_icon))) 34 | 35 | m = window.MainWindow() 36 | 37 | # for keyboard interrupts 38 | timer = QtCore.QTimer() 39 | timer.start(100) 40 | timer.timeout.connect(lambda: None) 41 | 42 | sys.exit(app.exec()) 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs~=1.4.4 2 | cx-Freeze==7.2.1 3 | evfl~=1.2.0 4 | keystone-engine~=0.9.2 5 | oead~=1.2.9.post4 6 | PySide6~=6.6.3 7 | PyYAML~=6.0.1 8 | quicktex~=0.2.1 9 | Pillow~=10.4.0 10 | -------------------------------------------------------------------------------- /roadmap.txt: -------------------------------------------------------------------------------- 1 | v0.5 2 | 3 | - eventflow edits through code gets pretty messy and hard to follow 4 | create our own type of flowchart file and write a parser that'll translate them into the .bfevfl files 5 | this reduces the amount of code and will just be plain easier to edit 6 | 7 | - create a Randomizer.bfevfl to handle giving all items, everything else will subflow to this 8 | made a branch for this, but rapids and seashell mansion crash for some reason, they might still need to be manual 9 | 10 | - keysanity 11 | use the Randomizer.bfevfl and the itemIndex parameter with the FlowControl::CompareInt to determine dungeon 12 | holding up dungeon items can just display the "Place:LvXDungeon" text, no need for custom text 13 | 14 | - barren dungeons 15 | 16 | ---------- 17 | v0.6 18 | 19 | - shuffle followers 20 | rooster implementation is easy, just need to compile a list of every room + the actor of the check 21 | bowwow has not been tested. Unlike the rooster, bowwow physically exists and needs to be touched 22 | if nothing works too well, we can always use custom text and require to go to a telephone booth 23 | 24 | - shop 25 | needs ASM changes to set the gettingFlag of the stolen item 26 | once set, we can simply just check for the flags in the post steal eventflow 27 | 28 | ---------- 29 | v0.7 30 | 31 | - trendy 32 | needs .bfres editing, at least enough to rename the model & file 33 | the format is Prize{prizeKey}.bfres, with prizeKey also serving as the model name 34 | might be possible to rename NpcMarin.bfres for example as PrizeMarin.bfres, and have the shop item called Marin? 35 | may not need .bfres editing if so 36 | 37 | - randomized trendy physics 38 | 39 | ---------- 40 | GENERAL FUTURE STUFF TO LOOK INTO 41 | 42 | - write asm for starting with maps, compasses, beaks 43 | it's fine to handle through events right now, this will just result in less written code 44 | 45 | - work on custom text 46 | use custom text to make tunics more intuitive 47 | 48 | - make every telephone booth have bowwow and rooster to swap between 49 | 50 | - individual tricks toggles 51 | 52 | - better workaround for slime key 53 | 54 | - make separete repo containing custom models to be used optionally with rando 55 | for songs, capacity upgrades, and even custom items 56 | we could also potentially use EoW models if the user has the RomFS 57 | 58 | - fix sunken heart pieces 59 | needs asm to make heart pieces ignore inventory (or just allow indexes past 31) 60 | then all heart pieces will be changed back into heart piece actors 61 | 62 | - randomize seashells 63 | FINISH PARTIAL SEASHELL SUPPORT 64 | will be by far the most complex asm 65 | seashells need to ignore inventory as well as add actor params to the ones dynamically spawned in 66 | 67 | - write ASM to make ItemSmallKey actors check parameter[1] like ObjTreasureBox does 68 | 69 | - fix dampe logic for instrument shuffle 70 | logic currently assumes that if you have the instrument, you have all the items needed to clear the rooms 71 | this is obviously not true with instrument shuffle, but logic is complex and I do not care enough for it 72 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from cx_Freeze import setup, Executable 3 | from RandomizerCore.randomizer_data import VERSION 4 | 5 | build_exe_options = {"packages": ["os"], 6 | "excludes": ["tkinter", "unittest", "sqlite3", "numpy", "matplotlib", "zstandard"], 7 | "zip_include_packages": ["encodings", "PySide6"], 8 | "include_files": ["RandomizerCore/Data", "RandomizerUI/Resources", "version.txt"], 9 | "optimize": 2} 10 | 11 | base = None 12 | if sys.platform == "win32": 13 | base = "Win32GUI" 14 | 15 | build_icon = "RandomizerUI/Resources/icon.ico" 16 | if sys.platform == "darwin": # mac 17 | build_icon = "RandomizerUI/Resources/icon.icns" 18 | 19 | setup( 20 | name = "Links Awakening Switch Randomizer", 21 | version = f"{VERSION}", 22 | description = "A randomizer for The Legend of Zelda: Link's Awakening remake!", 23 | options = {"build_exe": build_exe_options}, 24 | executables = [Executable("randomizer.py", base=base, target_name="Links Awakening Switch Randomizer", icon=build_icon)] 25 | ) -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.41 2 | --------------------------------------------------------------------------------