├── .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 |
--------------------------------------------------------------------------------