diff --git a/.github/actions/appversion-prepare/action.yml b/.github/actions/appversion-prepare/action.yml new file mode 100644 index 00000000..a8abdfde --- /dev/null +++ b/.github/actions/appversion-prepare/action.yml @@ -0,0 +1,51 @@ +name: Prepare AppVersion +description: Prepare AppVersion document for later use + +runs: + using: "composite" + steps: + # checkout commit + - name: Checkout commit + shell: bash + run: | + echo "Checkout commit" + - name: Checkout commit + uses: actions/checkout@v4.1.4 + + # Set Run Number + - name: Set Run Number + shell: bash + run: | + echo "Set Run Number" + - name: Set Run Number + id: set_run_number + shell: bash + run: | + GITHUB_RUN_NUMBER="${{ github.run_number }}a${{ github.run_attempt }}" + echo "github_run_number=$GITHUB_RUN_NUMBER" >> $GITHUB_OUTPUT + + # Prepare AppVersion + #TODO: source/classes/appversion.py writes the tag format + - name: 💬Prepare AppVersion + shell: bash + run: | + echo "💬Prepare AppVersion" + - name: Prepare AppVersion + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + GITHUB_RUN_NUMBER: ${{ steps.set_run_number.outputs.github_run_number }} + run: | + python -m source.classes.appversion + python ./resources/ci/common/prepare_appversion.py + + # upload appversion artifact for later step + - name: 🔼Upload AppVersion Artifact + shell: bash + run: | + echo "🔼Upload AppVersion Artifact" + - name: 🔼Upload AppVersion Artifact + uses: actions/upload-artifact@v4.3.3 + with: + name: appversion + path: ./resources/app/meta/manifests/app_version.txt diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 00000000..728e0c12 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,88 @@ +name: Build +description: Build app +inputs: + calling-job: + required: true + description: Job that's calling this one + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +runs: + using: "composite" + steps: + # checkout commit + - name: Checkout commit + shell: bash + run: | + echo "Checkout commit" + - name: Checkout commit + uses: actions/checkout@v4.1.4 + + # get parent dir + - name: Get Parent Directory + id: parentDir + uses: ./.github/actions/get-parent-dir + + # try to get UPX + - name: Get UPX + shell: bash + run: | + echo "Get UPX" + - name: Get UPX + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + UPX_VERSION: "4.2.3" + run: | + python ./resources/ci/common/get_upx.py + + # run build.py + - name: 💬Build Binaries + shell: bash + run: | + echo "💬Build Binaries" + - name: Build Binaries + shell: bash + run: | + pip install pyinstaller + python -m source.meta.build + + # upload problem children + # - name: 🔼Upload Problem Children Artifact + # shell: bash + # run: | + # echo "🔼Upload Problem Children Artifact" + # - name: 🔼Upload Problem Children Artifact + # uses: actions/upload-artifact@v4.3.3 + # with: + # name: problemchildren-${{ inputs.os-name }}-py${{ inputs.python-version }} + # path: ./resources/app/meta/manifests/excluded_dlls.json + # if-no-files-found: ignore # 'warn' or 'ignore' are also available, defaults to `warn` + + # prepare binary artifact for later step + - name: 💬Prepare Binary Artifact + shell: bash + run: | + echo "💬Prepare Binary Artifact" + - name: Prepare Binary Artifact + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + run: | + python ./resources/ci/common/prepare_binary.py + + # upload binary artifact for later step + - name: 🔼Upload Binary Artifact + shell: bash + run: | + echo "🔼Upload Binary Artifact" + - name: 🔼Upload Binary Artifact + uses: actions/upload-artifact@v4.3.3 + with: + name: binary-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/artifact + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` diff --git a/.github/actions/get-parent-dir/action.yml b/.github/actions/get-parent-dir/action.yml new file mode 100644 index 00000000..0194f3f1 --- /dev/null +++ b/.github/actions/get-parent-dir/action.yml @@ -0,0 +1,41 @@ +name: 📁Get Parent Directory +description: Get Parent Directory + +outputs: + parentDirNotWin: + description: "Parent Directory (!Windows)" + value: ${{ steps.parentDirNotWin.outputs.value }} + parentDir: + description: "Parent Directory (Windows)" + value: ${{ steps.parentDir.outputs.value }} + +######### +# actions +######### +# mad9000/actions-find-and-replace-string@5 + +runs: + using: "composite" + steps: + # get parent directory + - name: Get Repo Name + uses: mad9000/actions-find-and-replace-string@5 + id: repoName + with: + source: ${{ github.repository }} + find: "${{ github.repository_owner }}/" + replace: "" + - name: 📁Get Parent Directory Path (!Windows) + uses: mad9000/actions-find-and-replace-string@5 + id: parentDirNotWin + with: + source: ${{ github.workspace }} + find: "${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}" + replace: ${{ steps.repoName.outputs.value }} + - name: 📁Get Parent Directory Path (Windows) + uses: mad9000/actions-find-and-replace-string@5 + id: parentDir + with: + source: ${{ steps.parentDirNotWin.outputs.value }} + find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' + replace: ${{ steps.repoName.outputs.value }} diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml new file mode 100644 index 00000000..7a062c2d --- /dev/null +++ b/.github/actions/install/action.yml @@ -0,0 +1,49 @@ +name: 💿Install +description: Install app +inputs: + calling-job: + required: true + description: Job that's calling this one + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +######### +# actions +######### +# actions/checkout@v4.1.4 +# actions/setup-python@v5.1.0 +# actions/upload-artifact@v4.3.3 + +runs: + using: "composite" + steps: + # install python + - name: 💿Install Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ inputs.python-version }} + # install modules via pip + - name: 💿Install Modules + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + run: | + echo "Install Modules" + python ./resources/ci/common/get_pipline.py + # print pipline + - name: PipLine + shell: bash + run: | + echo "PipLine" + cat ./resources/user/meta/manifests/pipline.txt + # upload pipline + - name: 🔼Upload PipLine + uses: actions/upload-artifact@v4.3.3 + with: + name: pipline-${{ inputs.calling-job }}-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./resources/user/meta/manifests + if: contains(inputs.calling-job, 'test') diff --git a/.github/actions/release-prepare/action.yml b/.github/actions/release-prepare/action.yml new file mode 100644 index 00000000..05bf9a65 --- /dev/null +++ b/.github/actions/release-prepare/action.yml @@ -0,0 +1,77 @@ +name: 📀->📦Prepare Release +description: Prepare Release for Deployment +inputs: + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +######### +# actions +######### +# Artheau/SpriteSomething/get-parent-dir +# actions/checkout@v4.1.4 +# actions/download-artifact@v4.1.7 + +runs: + using: "composite" + steps: + # checkout commit + - name: ✔️Checkout commit + shell: bash + run: | + echo "✔️Checkout commit" + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + + # get parent dir + - name: 📁Get Parent Directory + shell: bash + run: | + echo "📁Get Parent Directory" + - name: 📁Get Parent Directory + id: parentDir + uses: ./.github/actions/get-parent-dir + + # download binary artifact + - name: 🔽Download Binary Artifact + shell: bash + run: | + echo "🔽Download Binary Artifact" + - name: 🔽Download Binary Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: binary-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./ + + # download appversion artifact + - name: 🔽Download AppVersion Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: appversion + path: ${{ steps.parentDir.outputs.parentDir }}/build + + # Prepare Release + - name: 💬Prepare Release + shell: bash + run: | + echo "💬Prepare Release" + - name: Prepare Release + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + run: | + python ./resources/ci/common/prepare_release.py + + # upload archive artifact for later step + - name: 🔼Upload Archive Artifact + shell: bash + run: | + echo "🔼Upload Archive Artifact" + - name: 🔼Upload Archive Artifact + uses: actions/upload-artifact@v4.3.3 + with: + name: archive-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy diff --git a/.github/actions/tag-repo/action.yml b/.github/actions/tag-repo/action.yml new file mode 100644 index 00000000..f4c2db50 --- /dev/null +++ b/.github/actions/tag-repo/action.yml @@ -0,0 +1,76 @@ +name: 🏷️Tag Repository +description: Tag a repository + +inputs: + repository: + description: "Repository Owner/Name; octocat/Hello-World" + required: true + ref-name: + description: "Reference name; branch, tag, etc" + required: true + github-tag: + description: "Reference to tag with" + required: true + debug: + description: "Debug Mode, won't set tag" + required: false + default: "false" + +runs: + using: "composite" + steps: + - name: 🏷️Tag Repository + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ env.FINE_PAT }} + script: | + const debug = ${{ inputs.debug }} == "true" || ${{ inputs.debug }} == true; + const repository = '${{ inputs.repository }}'; + const owner = repository.substring(0,repository.indexOf('/')); + const repo = repository.substring(repository.indexOf('/')+1); + const ref = '${{ inputs.ref-name }}'; + // get git tag + const gitTag = '${{ inputs.github-tag }}'; + console.log('Repo Data: ', `${owner}/${repo}@${ref}`) + console.log('Git tag: ', gitTag) + if(gitTag == '') { + let msg = 'Result: 🔴No Git Tag sent, aborting!'; + console.log(msg) + core.setFailed(msg) + return + } + // get latest commit + const latestCommit = await github.rest.git.getRef({ + owner: owner, + repo: repo, + ref: ref + }) + // get latest refs + const latestRefs = await github.rest.git.listMatchingRefs({ + owner: owner, + repo: repo + }) + let latestTag = ''; // bucket for latest tag + // get last tag in data + for(let thisRef of latestRefs.data) { + if(thisRef['ref'].indexOf('tags') > -1) { + let refParts = thisRef['ref'].split('/'); + latestTag = refParts[-1]; + } + } + console.log('Latest tag:', latestTag) + if(latestTag != gitTag) { + if(debug) { + console.log(`DEBUG: 🔵Creating '${gitTag}' tag`) + } else { + console.log(`Result: 🟢Creating '${gitTag}' tag`) + github.rest.git.createRef({ + owner: owner, + repo: repo, + ref: `refs/tags/${gitTag}`, + sha: latestCommit.data.object.sha + }) + } + } else { + console.log('Result: 🟡Not creating release tag') + } diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 00000000..95db06db --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,97 @@ +name: ⏱️Test +description: Test app +inputs: + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +######### +# actions +######### +# actions/checkout@v4.1.4 +# actions/download-artifact@v4.1.7 +# actions/upload-artifact@v4.3.3 +# coactions/setup-xvfb@v1.0.1 + +runs: + using: "composite" + steps: + # download pipline + - name: 🔽Download PipLine + shell: bash + run: | + echo "🔽Download PipLine" + - name: 🔽Download PipLine + uses: actions/download-artifact@v4.1.7 + with: + name: pipline-test-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./resources/user/meta/manifests + + # run tests + - name: 🖥️Test Base + shell: bash + run: | + echo "🖥️Test Base" + - name: 🖥️Test Base + shell: bash + run: | + mkdir -p ./failures + echo "" > ./failures/errors.txt + python -m pip install tqdm + python ./test/NewTestSuite.py + # - name: 🖥️Test Mystery + # shell: bash + # run: | + # echo "🖥️Test Mystery" + # if: contains(inputs.os-name, 'macos') + # - name: 🖥️Test Mystery + # shell: bash + # run: | + # python ./test/MysteryTestSuite.py + # if: contains(inputs.os-name, 'macos') + + # upload logs + - name: 🔼Upload Logs + shell: bash + run: | + echo "🔼Upload Logs" + - name: 🔼Upload Logs + uses: actions/upload-artifact@v4.3.3 + with: + name: logs-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./logs + if-no-files-found: ignore + + # print failures + - name: 💬Print Failures + if: failure() + shell: bash + run: | + echo "💬Print Failures" + - name: Print Failures + if: failure() + shell: bash + run: | + ERR_STRING="$(cat ./failures/errors.txt)" + ERR_STRING="${ERR_STRING//'%'/'%25'}" + ERR_STRING="${ERR_STRING//$'\n'/' | '}" + ERR_STRING="${ERR_STRING//$'\r'/' | '}" + ERR_STRING="${ERR_STRING//$'\n'/'%0A'}" + ERR_STRING="${ERR_STRING//$'\r'/'%0D'}" + echo "::error ::$ERR_STRING" + + # upload failures + - name: 🔼Upload Failures + if: failure() + shell: bash + run: | + echo "🔼Upload Failures" + - name: 🔼Upload Failures + if: failure() + uses: actions/upload-artifact@v4.3.3 + with: + name: failures-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./failures diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ab6c8163..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,316 +0,0 @@ -# workflow name -name: Build - -# fire on -on: - push: - branches: - - DoorDev - pull_request: - branches: - - DoorDev - -# stuff to do -jobs: - # Install & Build - # Set up environment - # Build - # Run build-gui.py - # Run build-dr.py - install-build: - name: Install/Build - # cycle through os list - runs-on: ${{ matrix.os-name }} - - # VM settings - # os & python versions - strategy: - matrix: - os-name: [ ubuntu-latest, ubuntu-20.04, macOS-latest, windows-latest ] - python-version: [ 3.9 ] -# needs: [ install-test ] - steps: - # checkout commit - - name: Checkout commit - uses: actions/checkout@v3 - # install python - - name: Install python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - architecture: "x64" - - run: | - python --version - # install dependencies via pip - - name: Install dependencies via pip - env: - OS_NAME: ${{ matrix.os-name }} - run: | - python ./resources/ci/common/install.py - pip install pyinstaller - # get parent directory - - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@3 - id: repoName - with: - source: ${{ github.repository }} - find: '${{ github.repository_owner }}/' - replace: '' - - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDirNotWin - with: - source: ${{ github.workspace }} - find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDir - with: - source: ${{ steps.parentDirNotWin.outputs.value }} - find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - # try to get UPX - - name: Get UPX - env: - OS_NAME: ${{ matrix.os-name }} - run: | - python ./resources/ci/common/get_upx.py - # run build-gui.py - - name: Build GUI - run: | - python ./source/meta/build-gui.py - # run build-dr.py - - name: Build DungeonRandomizer - run: | - python ./source/meta/build-dr.py - # prepare binary artifacts for later step - - name: Prepare Binary Artifacts - env: - OS_NAME: ${{ matrix.os-name }} - run: | - python ./resources/ci/common/prepare_binary.py - # upload binary artifacts for later step - - name: Upload Binary Artifacts - uses: actions/upload-artifact@v3 - with: - name: binaries-${{ matrix.os-name }} - path: ${{ steps.parentDir.outputs.value }}/artifact - - # Install & Preparing Release - # Set up environment - # Local Prepare Release action - install-prepare-release: - name: Install/Prepare Release - # cycle through os list - runs-on: ${{ matrix.os-name }} - - # VM settings - # os & python versions - strategy: - matrix: - # install/release on not bionic - os-name: [ ubuntu-latest, ubuntu-20.04, macOS-latest, windows-latest ] - python-version: [ 3.9 ] - - needs: [ install-build ] - steps: - # checkout commit - - name: Checkout commit - uses: actions/checkout@v3 - # install python - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - architecture: "x64" - - run: | - python --version - # install dependencies via pip - - name: Install Dependencies via pip - env: - OS_NAME: ${{ matrix.os-name }} - run: | - python ./resources/ci/common/install.py - # get parent directory - - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@3 - id: repoName - with: - source: ${{ github.repository }} - find: '${{ github.repository_owner }}/' - replace: '' - - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDirNotWin - with: - source: ${{ github.workspace }} - find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDir - with: - source: ${{ steps.parentDirNotWin.outputs.value }} - find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - # download binary artifact - - name: Download Binary Artifact - uses: actions/download-artifact@v3 - with: - name: binaries-${{ matrix.os-name }} - path: ./ - # Prepare AppVersion & Release - - name: Prepare AppVersion & Release - env: - OS_NAME: ${{ matrix.os-name }} - run: | - python ./build-app_version.py - python ./resources/ci/common/prepare_appversion.py - python ./resources/ci/common/prepare_release.py - # upload appversion artifact for later step - - name: Upload AppVersion Artifact - uses: actions/upload-artifact@v3 - with: - name: appversion-${{ matrix.os-name }} - path: ./resources/app/meta/manifests/app_version.txt - # upload archive artifact for later step - - name: Upload Archive Artifact - uses: actions/upload-artifact@v3 - with: - name: archive-${{ matrix.os-name }} - path: ${{ steps.parentDir.outputs.value }}/deploy - - # Deploy to GitHub Releases - # Release Name: ALttPDoorRandomizer v${GITHUB_TAG} - # Release Body: Inline content of RELEASENOTES.md - # Release Body: Fallback to URL to RELEASENOTES.md - # Release Files: ${{ steps.parentDir.outputs.value }}/deploy - deploy-release: - name: Deploy GHReleases - runs-on: ${{ matrix.os-name }} - - # VM settings - # os & python versions - strategy: - matrix: - # release only on focal - os-name: [ ubuntu-latest ] - python-version: [ 3.9 ] - - needs: [ install-prepare-release ] - steps: - # checkout commit - - name: Checkout commit - uses: actions/checkout@v3 - # get parent directory - - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@3 - id: repoName - with: - source: ${{ github.repository }} - find: '${{ github.repository_owner }}/' - replace: '' - - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDirNotWin - with: - source: ${{ github.workspace }} - find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDir - with: - source: ${{ steps.parentDirNotWin.outputs.value }} - find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Install Dependencies via pip - run: | - python -m pip install pytz requests - # download appversion artifact - - name: Download AppVersion Artifact - uses: actions/download-artifact@v3 - with: - name: appversion-${{ matrix.os-name }} - path: ${{ steps.parentDir.outputs.value }}/build - # download ubuntu archive artifact - - name: Download Ubuntu Archive Artifact - uses: actions/download-artifact@v3 - with: - name: archive-ubuntu-latest - path: ${{ steps.parentDir.outputs.value }}/deploy/linux - # download macos archive artifact - - name: Download MacOS Archive Artifact - uses: actions/download-artifact@v3 - with: - name: archive-macOS-latest - path: ${{ steps.parentDir.outputs.value }}/deploy/macos - # download windows archive artifact - - name: Download Windows Archive Artifact - uses: actions/download-artifact@v3 - with: - name: archive-windows-latest - path: ${{ steps.parentDir.outputs.value }}/deploy/windows - # debug info - - name: Debug Info - id: debug_info -# shell: bash -# git tag ${GITHUB_TAG} -# git push origin ${GITHUB_TAG} - run: | - GITHUB_TAG="$(head -n 1 ../build/app_version.txt)" - echo "::set-output name=github_tag::$GITHUB_TAG" - GITHUB_TAG="v${GITHUB_TAG}" - RELEASE_NAME="ALttPDoorRandomizer ${GITHUB_TAG}" - echo "Release Name: ${RELEASE_NAME}" - echo "Git Tag: ${GITHUB_TAG}" - # create a pre/release - - name: Create a Pre/Release - id: create_release - uses: actions/create-release@v1.1.4 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ steps.debug_info.outputs.github_tag }} - release_name: ALttPDoorRandomizer v${{ steps.debug_info.outputs.github_tag }} - body_path: RELEASENOTES.md - draft: true - prerelease: true - if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease') - # upload linux archive asset - - name: Upload Linux Archive Asset - id: upload-linux-asset - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ../deploy/linux/ALttPDoorRandomizer.tar.gz - asset_name: ALttPDoorRandomizer-${{ steps.debug_info.outputs.github_tag }}-linux-focal.tar.gz - asset_content_type: application/gzip - if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease') - # upload macos archive asset - - name: Upload MacOS Archive Asset - id: upload-macos-asset - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ../deploy/macos/ALttPDoorRandomizer.tar.gz - asset_name: ALttPDoorRandomizer-${{ steps.debug_info.outputs.github_tag }}-osx.tar.gz - asset_content_type: application/gzip - if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease') - # upload windows archive asset - - name: Upload Windows Archive Asset - id: upload-windows-asset - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ../deploy/windows/ALttPDoorRandomizer.zip - asset_name: ALttPDoorRandomizer-${{ steps.debug_info.outputs.github_tag }}-windows.zip - asset_content_type: application/zip - if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease') diff --git a/.github/workflows/release-complete.yml b/.github/workflows/release-complete.yml new file mode 100644 index 00000000..a193e0ec --- /dev/null +++ b/.github/workflows/release-complete.yml @@ -0,0 +1,47 @@ +# workflow name +name: 🏷️Tag Repositories + +# Fine-grained personal access token +# https://github.com/settings/tokens?type=beta +# token needs perms: +# actions: read/write +# commit statuses: read/write +# contents: read/write +# workflows: read/write +# copy token +# Actions secrets and variables +# github.com///settings/secrets/actions +# repository secret +# name a new secret "ALTTPER_TAGGER" +# value set to copied token + +# fire on +on: + release: + types: + - released + +jobs: + # Tag Baserom + tag-baserom: + name: 🖳Tag Baserom + runs-on: ${{ matrix.os-name }} + strategy: + matrix: + os-name: [ + # ubuntu-latest + "ubuntu-22.04" + ] + + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + - name: 🏷️Tag Repository + uses: ./.github/actions/tag-repo + env: + FINE_PAT: ${{ secrets.ALTTPER_TAGGER }} + with: + repository: ${{ github.repository_owner }}/z3randomizer + ref-name: heads/OWMain + github-tag: ${{ github.event.release.tag_name }} diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml new file mode 100644 index 00000000..5942ff1b --- /dev/null +++ b/.github/workflows/release-create.yml @@ -0,0 +1,414 @@ +# workflow name +name: ⏱️Test/🔨Build/🚀Deploy + +# fire on +on: [ + push, + pull_request +] + +# on: +# push: +# branches: +# - DoorDevUnstable +# - DoorDev +# - OverworldShuffleDev +# - OverworldShuffle +# pull_request: +# branches: +# - DoorDevUnstable +# - DoorDev +# - OverworldShuffleDev +# - OverworldShuffle + +# stuff to do +jobs: + # Diagnostics + diags: + # diagnostics + # call checkout + # call install python + # print python version + # call install + # call analyze github actions + # install extra python modules + # run diagnostics + name: 🧮 + runs-on: ${{ matrix.os-name }} + continue-on-error: True + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install python + - name: 💿Install Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + # print python version + - name: 🐍Python Version + shell: bash + run: | + python --version + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: diags + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call analyze github actions + - name: ⚙️Analyze used GitHub Actions + shell: bash + run: | + python ./resources/ci/common/list_actions.py + # install extra python modules + - name: 💿Install extra Python Modules + shell: bash + run: | + python -m pip install setuptools + # run diagnostics + - name: 🧮Print Diagnostics + shell: bash + run: | + python -m source.meta.run_diags + + # Test + install-test: + # test + # call checkout + # call install + # run tests + name: 💿/⏱️ + runs-on: ${{ matrix.os-name }} + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: test + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call test + - name: ⏱️Call Test + uses: ./.github/actions/test + with: + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + + # Prepare AppVersion + appversion-prepare: + # prepare appversion + # call checkout + # call install + # call appversion-prepare + name: 💬 + runs-on: ${{ matrix.os-name }} + needs: [install-test] + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: appversion-prepare + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call appversion-prepare + - name: 💬Call Prepare AppVersion + uses: ./.github/actions/appversion-prepare + + # Build + install-build: + # build + # call checkout + # call install + # call build + name: 💿/🔨 + runs-on: ${{ matrix.os-name }} + needs: [appversion-prepare] + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: build + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call build + - name: 🔨Call Build + uses: ./.github/actions/build + with: + calling-job: build + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + + # Prepare Release + release-prepare: + # prepare release + # call checkout + # install extra python modules + # call prepare release + name: 💿/📀->📦 + runs-on: ${{ matrix.os-name }} + needs: [install-build] + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # install extra python modules + - name: 💿Install extra Python Modules + shell: bash + run: | + python -m pip install setuptools + # call prepare release + - name: 📀->📦Prepare Release + uses: ./.github/actions/release-prepare + with: + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + + # Deploy Release + # Needs to be top-level for SECRET to work easily + release-deploy: + name: 📀->🚀 + runs-on: ${{ matrix.os-name }} + needs: [release-prepare] + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + + steps: + # checkout commit + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + + # install extra python modules + - name: 💿Install extra Python Modules + shell: bash + run: | + python -m pip install pytz requests + + # get parent dir + - name: 📁Get Parent Directory + id: parentDir + uses: ./.github/actions/get-parent-dir + + # download appversion artifact + - name: 🔽Download AppVersion Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: appversion + path: ${{ steps.parentDir.outputs.parentDir }}/build + + # download ubuntu archive artifact + - name: 🔽Download Ubuntu Archive Artifact + uses: actions/download-artifact@v4.1.7 + with: + # should run on latest explicit ubuntu version + name: archive-ubuntu-22.04-py${{ matrix.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy/linux + + # download macos archive artifact + - name: 🔽Download MacOS Archive Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: archive-macos-latest-py${{ matrix.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy/macos + + # download windows archive artifact + - name: 🔽Download Windows Archive Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: archive-windows-latest-py${{ matrix.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy/windows + + # determine linux archive asset + - name: ❔Identify Linux Archive Asset + id: identify-linux-asset + shell: bash + run: | + ASSET_LINUX="$(ls ${{ steps.parentDir.outputs.parentDir }}/deploy/linux)" + echo "asset_linux=$ASSET_LINUX" >> $GITHUB_OUTPUT + + # determine macos archive asset + - name: ❔Identify MacOS Archive Asset + id: identify-macos-asset + shell: bash + run: | + ASSET_MACOS="$(ls ${{ steps.parentDir.outputs.parentDir }}/deploy/macos)" + echo "asset_macos=$ASSET_MACOS" >> $GITHUB_OUTPUT + + # determine windows archive asset + - name: ❔Identify Windows Archive Asset + id: identify-windows-asset + shell: bash + run: | + ASSET_WIN="$(ls ${{ steps.parentDir.outputs.parentDir }}/deploy/windows)" + echo "asset_windows=$ASSET_WIN" >> $GITHUB_OUTPUT + + # archive listing + # - name: Archive Listing + # shell: bash + # run: | + # ls -R ${{ steps.parentDir.outputs.parentDir }}/deploy/ + + # debug info + #TODO: Project Name + - name: 📝Debug Info + id: debug_info + run: | + PROJECT_NAME="ALttPOverworldRandomizer" + echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT + + GITHUB_TAG="$(head -n 1 ../build/app_version.txt)" + echo "github_tag=$GITHUB_TAG" >> $GITHUB_OUTPUT + + RELEASE_NAME="${PROJECT_NAME} ${GITHUB_TAG}" + echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT + + ASSET_PREFIX="${PROJECT_NAME}-${GITHUB_TAG}" + echo "asset_prefix=$ASSET_PREFIX" >> $GITHUB_OUTPUT + + echo "Project Name: ${PROJECT_NAME}" + echo "Release Name: ${RELEASE_NAME}" + echo "Asset Prefix: ${ASSET_PREFIX}" + echo "Git Tag: ${GITHUB_TAG}" + echo "Linux Asset: ${{ steps.identify-linux-asset.outputs.asset_linux }}" + echo "MacOS Asset: ${{ steps.identify-macos-asset.outputs.asset_macos }}" + echo "Windows Asset: ${{ steps.identify-windows-asset.outputs.asset_windows }}" + + # create a release (MASTER) + - name: 📀->🚀Create a Release (MASTER) + id: create_release + uses: actions/create-release@v1.1.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.debug_info.outputs.github_tag }} + release_name: ${{ steps.debug_info.outputs.release_name }} + body_path: RELEASENOTES.md + # draft: true + if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + + # upload linux archive asset (MASTER) + #TODO: Make sure we're firing on the proper branches + - name: 🔼Upload Linux Archive Asset (MASTER) + id: upload-linux-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/linux/${{ steps.identify-linux-asset.outputs.asset_linux }} + asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-linux-focal.tar.gz + asset_content_type: application/gzip + if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + + # upload macos archive asset (MASTER) + #TODO: Make sure we're firing on the proper branches + - name: 🔼Upload MacOS Archive Asset (MASTER) + id: upload-macos-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/macos/${{ steps.identify-macos-asset.outputs.asset_macos }} + asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-osx.tar.gz + asset_content_type: application/gzip + if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + + # upload windows archive asset (MASTER) + #TODO: Make sure we're firing on the proper branches + - name: 🔼Upload Windows Archive Asset (MASTER) + id: upload-windows-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/windows/${{ steps.identify-windows-asset.outputs.asset_windows }} + asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-windows.zip + asset_content_type: application/zip + if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message diff --git a/.gitignore b/.gitignore index a2d08623..0adf6d06 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ *.bst *.wixobj *.bat -build +/build bundle/components.wxs dist README.html @@ -40,7 +40,10 @@ resources/user/* get-pip.py venv -test +/test test_games/ data/sprites/official/selan.1.zspr *.zspr + +*errors.txt +*success.txt diff --git a/BaseClasses.py b/BaseClasses.py index cf0432a5..c9c90090 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -15,12 +15,13 @@ from Utils import int16_as_bytes from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from RoomData import Room from source.dungeon.RoomObject import RoomObject +from source.overworld.EntranceData import door_addresses class World(object): def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, - timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints): + timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): self.players = players self.teams = 1 self.owShuffle = owShuffle.copy() @@ -78,6 +79,7 @@ class World(object): self.prizes = {} self.dynamic_regions = [] self.dynamic_locations = [] + self.spoiler_mode = spoiler_mode self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 self.owedges = [] @@ -87,6 +89,7 @@ class World(object): self.owwhirlpools = {} self.owflutespots = {} self.owsectors = {} + self.allow_flip_sanc = {} self.doors = [] self._door_cache = {} self.paired_doors = {} @@ -106,6 +109,7 @@ class World(object): self.data_tables = {} self.damage_table = {} + for player in range(1, players + 1): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val @@ -116,6 +120,7 @@ class World(object): set_player_attr('owcrossededges', []) set_player_attr('owwhirlpools', []) set_player_attr('owsectors', None) + set_player_attr('allow_flip_sanc', False) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('bottle_refills', ['Bottle (Green Potion)', 'Bottle (Green Potion)']) @@ -138,6 +143,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', 'none') set_player_attr('bigkeyshuffle', False) + set_player_attr('prizeshuffle', 'none') set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) set_player_attr('flute_mode', False) @@ -316,6 +322,11 @@ class World(object): def is_bombshop_start(self, player): return self.is_tile_swapped(0x2c, player) + def is_dark_chapel_start(self, player): + if self.allow_flip_sanc[player]: + return self.is_tile_swapped(0x13, player) + return self.mode[player] == 'inverted' + def is_pyramid_open(self, player): if self.open_pyramid[player] == 'yes': return True @@ -464,7 +475,8 @@ class World(object): def push_precollected(self, item): item.world = self - if ((item.smallkey and self.keyshuffle[item.player] != 'none') + if ((item.prize and self.prizeshuffle[item.player] != 'none') + or (item.smallkey and self.keyshuffle[item.player] != 'none') or (item.bigkey and self.bigkeyshuffle[item.player])): item.advancement = True self.precollected_items.append(item) @@ -690,7 +702,7 @@ class CollectionState(object): bc[conn] = door_crystal_state queue.append((conn, door_crystal_state)) elif door is None: - # note: no door in dungeon indicates what exactly? (always traversable)? + bc[conn] = new_crystal_state queue.append((conn, new_crystal_state)) else: new_crystal_state = CrystalBarrier.Orange @@ -1598,7 +1610,8 @@ class Region(object): inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none') or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) - or (item.compass and not self.world.compassshuffle[item.player])) + or (item.compass and not self.world.compassshuffle[item.player]) + or (item.prize and self.world.prizeshuffle[item.player] == 'dungeon')) # not all small keys to escape must be in escape # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' if inside_dungeon_item: @@ -1831,6 +1844,7 @@ class Dungeon(object): def __init__(self, name, regions, big_key, small_keys, dungeon_items, player, dungeon_id): self.name = name self.regions = regions + self.prize = None self.big_key = big_key self.small_keys = small_keys self.dungeon_items = dungeon_items @@ -1855,10 +1869,13 @@ class Dungeon(object): @property def all_items(self): - return self.dungeon_items + self.keys + return self.dungeon_items + self.keys + ([self.prize] if self.prize else []) def is_dungeon_item(self, item): - return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] + if item.prize: + return item.player == self.player and self.prize is None and self.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower'] + else: + return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] def count_dungeon_item(self): return len(self.dungeon_items) + 1 if self.big_key_required else 0 + self.key_number @@ -2612,7 +2629,7 @@ class Boss(object): class Location(object): - def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, forced_item=None, + def __init__(self, player, name='', address=None, prize=False, hint_text=None, parent=None, forced_item=None, player_address=None, note=None): self.name = name self.parent_region = parent @@ -2626,7 +2643,7 @@ class Location(object): self.forced_item = None self.item = None self.event = False - self.crystal = crystal + self.prize = prize self.address = address self.player_address = player_address self.spot_type = 'Location' @@ -2634,14 +2651,14 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.locked = False - self.real = not crystal + self.real = not prize self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.verbose_rule = None self.item_rule = lambda item: True self.player = player self.skip = False - self.type = LocationType.Normal if not crystal else LocationType.Prize + self.type = LocationType.Normal if not prize else LocationType.Prize self.pot = None self.drop = None self.note = note @@ -2729,8 +2746,8 @@ class Item(object): self.player = player @property - def crystal(self): - return self.type == 'Crystal' + def prize(self): + return self.type == 'Prize' @property def smallkey(self): @@ -2758,11 +2775,33 @@ class Item(object): return item_dungeon def is_inside_dungeon_item(self, world): - return ((self.smallkey and world.keyshuffle[self.player] == 'none') + return ((self.prize and world.prizeshuffle[self.player] in ['none', 'dungeon']) + or (self.smallkey and world.keyshuffle[self.player] == 'none') or (self.bigkey and not world.bigkeyshuffle[self.player]) or (self.compass and not world.compassshuffle[self.player]) or (self.map and not world.mapshuffle[self.player])) + def get_map_location(self): + if self.location: + if self.location.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + return self.location + else: + def explore_region(region): + explored_regions.append(region.name) + for ent in region.entrances: + if ent.parent_region is not None: + if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + return ent + elif ent.parent_region.name not in explored_regions: + ret = explore_region(ent.parent_region) + if ret: + return ret + return None + explored_regions = list() + return explore_region(self.location.parent_region) + + return None + def __str__(self): return str(self.__unicode__()) @@ -2809,7 +2848,6 @@ class Shop(object): # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] entrances = self.region.entrances config = self.item_count - from EntranceShuffle import door_addresses if len(entrances) == 1 and entrances[0].name in door_addresses: door_id = door_addresses[entrances[0].name][0] + 1 else: @@ -2869,6 +2907,17 @@ class Spoiler(object): self.metadata = {} self.shops = [] self.bosses = OrderedDict() + if world.spoiler_mode == 'settings': + self.settings = {'settings'} + elif world.spoiler_mode == 'semi': + self.settings = {'settings', 'entrances', 'requirements', 'prizes'} + elif world.spoiler_mode == 'full': + self.settings = {'all'} + elif world.spoiler_mode == 'debug': + self.settings = {'all', 'debug'} + else: + self.settings = {} + self.suppress_spoiler_locations = ['Big Bomb', 'Frog', 'Dark Blacksmith Ruins', 'Middle Aged Man', 'Lost Old Man', 'Old Man Drop Off'] @@ -2963,6 +3012,7 @@ class Spoiler(object): 'compassshuffle': self.world.compassshuffle, 'keyshuffle': self.world.keyshuffle, 'bigkeyshuffle': self.world.bigkeyshuffle, + 'prizeshuffle': self.world.prizeshuffle, 'boss_shuffle': self.world.boss_shuffle, 'enemy_shuffle': self.world.enemy_shuffle, 'enemy_health': self.world.enemy_health, @@ -3003,6 +3053,13 @@ class Spoiler(object): self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] + self.prizes = OrderedDict() + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + for dungeon in self.world.dungeons: + if dungeon.player == player and dungeon.prize: + self.prizes[dungeon.name + player_name] = dungeon.prize.name + self.bottles = OrderedDict() if self.world.players == 1: self.bottles['Waterfall Bottle'] = self.world.bottle_refills[1][0] @@ -3012,27 +3069,29 @@ class Spoiler(object): self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0] self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] + def include_item(item): + return 'all' in self.settings or ('items' in self.settings and not item.prize) or ('prizes' in self.settings and item.prize) self.locations = OrderedDict() listed_locations = set() - lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and not loc.skip] + lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and not loc.skip and include_item(loc.item)] self.locations['Light World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations]) listed_locations.update(lw_locations) - dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and not loc.skip] + dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and not loc.skip and include_item(loc.item)] self.locations['Dark World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations]) listed_locations.update(dw_locations) - cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip] + cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip and include_item(loc.item)] self.locations['Caves'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations]) listed_locations.update(cave_locations) for dungeon in self.world.dungeons: - dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip] + dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip and include_item(loc.item)] self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations]) listed_locations.update(dungeon_locations) - other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip] + other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip and include_item(loc.item)] if other_locations: self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations]) listed_locations.update(other_locations) @@ -3132,85 +3191,88 @@ class Spoiler(object): outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v) if self.metadata['user_notes']: outfile.write('User Notes:'.ljust(line_width) + '%s\n' % self.metadata['user_notes']) - outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm) + if 'all' in self.settings or 'settings' in self.settings: + outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm) outfile.write('Players:'.ljust(line_width) + '%d\n' % self.world.players) outfile.write('Teams:'.ljust(line_width) + '%d\n' % self.world.teams) for player in range(1, self.world.players + 1): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) - outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player]) + if 'all' in self.settings or 'settings' in self.settings: + outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player]) outfile.write('\n') - outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) - outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player]) - outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player]) - if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: - outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) - outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) - outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) - outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) - outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) - outfile.write('\n') - outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) - outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player]) - outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) - outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) - outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player]) - outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player]) - outfile.write('Beemizer:'.ljust(line_width) + '%s\n' % self.metadata['beemizer'][player]) - outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player])) - outfile.write('\n') - outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player])) - outfile.write('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) - outfile.write('Pottery Mode:'.ljust(line_width) + '%s\n' % self.metadata['pottery'][player]) - outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player])) - outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) - outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) - outfile.write('\n') - outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla': - outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) - outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': - outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) - outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) - outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) - outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) - outfile.write('\n') - outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) - if self.metadata['shuffle'][player] != 'vanilla': - outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) - outfile.write('Shuffle Link\'s House:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) - outfile.write('Shuffle Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player])) - outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) - if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]: + if 'all' in self.settings or 'settings' in self.settings: + outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) + outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player]) + outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player]) + if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: + outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) + outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) + outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) + outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) + outfile.write('\n') + outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) + outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player]) + outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) + outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) + outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player]) + outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player]) + outfile.write('Beemizer:'.ljust(line_width) + '%s\n' % self.metadata['beemizer'][player]) + outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player])) + outfile.write('\n') + outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player])) + outfile.write('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) + outfile.write('Pottery Mode:'.ljust(line_width) + '%s\n' % self.metadata['pottery'][player]) + outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player])) + outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) + outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) + outfile.write('\n') + outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) + if self.metadata['ow_shuffle'][player] != 'vanilla': + outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) + outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) + if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': + outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) + outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) + outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) + outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) + outfile.write('\n') + outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write('Shuffle Link\'s House:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) + outfile.write('Shuffle Back of Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player])) + outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) + outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) - outfile.write('\n') - outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) - outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) - outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) - outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) - outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) - outfile.write('\n') - outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) - if self.metadata['door_shuffle'][player] != 'vanilla': - outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) - outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player]) - outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player]) - outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player])) - outfile.write('Spiral Stairs Self-Loop:'.ljust(line_width) + '%s\n' % yn(self.metadata['door_self_loops'][player])) - outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) - outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) - outfile.write('\n') - outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) - outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) - if self.metadata['enemy_shuffle'][player] != 'none': - outfile.write('Enemy Logic:'.ljust(line_width) + '%s\n' % self.metadata['any_enemy_logic'][player]) - outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) - outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) - outfile.write('\n') - outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) - outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) - outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) + outfile.write('\n') + outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) + outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) + outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) + outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) + outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player]) + outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) + outfile.write('\n') + outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) + outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player]) + outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player]) + outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player])) + outfile.write('Spiral Stairs Self-Loop:'.ljust(line_width) + '%s\n' % yn(self.metadata['door_self_loops'][player])) + outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) + outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) + outfile.write('\n') + outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) + outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) + outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) + outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) + if self.metadata['enemy_shuffle'][player] != 'none': + outfile.write('Enemy Logic:'.ljust(line_width) + '%s\n' % self.metadata['any_enemy_logic'][player]) + outfile.write('\n') + outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) + outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) + outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) @@ -3247,70 +3309,77 @@ class Spoiler(object): self.parse_data() with open(filename, 'a') as outfile: line_width = 35 - outfile.write('\n\nRequirements:\n\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) - for player in range(1, self.world.players + 1): - player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') - if self.world.crystals_gt_orig[player] == 'random': - outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) - if self.world.crystals_ganon_orig[player] == 'random': - outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - - outfile.write('\n\nBottle Refills:\n\n') - for fairy, bottle in self.bottles.items(): - outfile.write(f'{fairy}: {bottle}\n') + if 'all' in self.settings or 'requirements' in self.settings: + outfile.write('\n\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + if self.world.crystals_gt_orig[player] == 'random': + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + outfile.write('\n\nPrizes:\n\n') + for dungeon, prize in self.prizes.items(): + outfile.write(str(dungeon + ':').ljust(line_width) + '%s\n' % prize) + + if 'all' in self.settings or 'misc' in self.settings: + outfile.write('\n\nBottle Refills:\n\n') + for fairy, bottle in self.bottles.items(): + outfile.write(f'{fairy}: {bottle}\n') if self.maps: - # flute shuffle - for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - outfile.write('\n\nFlute Spots:\n\n') - break - for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('flute', player)]['text']) + if 'all' in self.settings or 'flute' in self.settings: + # flute shuffle + for player in range(1, self.world.players + 1): + if ('flute', player) in self.maps: + outfile.write('\n\nFlute Spots:\n\n') + break + for player in range(1, self.world.players + 1): + if ('flute', player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[('flute', player)]['text']) - # overworld tile flips - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - outfile.write('\n\nOW Tile Flips:\n\n') - break - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('swaps', player)]['text']) - - # crossed groups - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - outfile.write('\n\nOW Crossed Groups:\n\n') - break - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('groups', player)]['text']) + if 'all' in self.settings or 'overworld' in self.settings: + # overworld tile flips + for player in range(1, self.world.players + 1): + if ('swaps', player) in self.maps: + outfile.write('\n\nOW Tile Flips:\n\n') + break + for player in range(1, self.world.players + 1): + if ('swaps', player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[('swaps', player)]['text']) + + # crossed groups + for player in range(1, self.world.players + 1): + if ('groups', player) in self.maps: + outfile.write('\n\nOW Crossed Groups:\n\n') + break + for player in range(1, self.world.players + 1): + if ('groups', player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[('groups', player)]['text']) - if self.overworlds: + if self.overworlds and ('all' in self.settings or 'overworld' in self.settings): outfile.write('\n\nOverworld Edges:\n\n') # overworld transitions outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) - if self.whirlpools: + if self.whirlpools and ('all' in self.settings or 'overworld' in self.settings): outfile.write('\n\nWhirlpools:\n\n') # whirlpools outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","whirlpools",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","whirlpools",entry['exit'])) for entry in self.whirlpools.values()])) - if self.entrances: + if self.entrances and ('all' in self.settings or 'entrances' in self.settings): # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly outfile.write('\n\nEntrances:\n\n') outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta", "entrances", entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta", "entrances", entry['exit'])) for entry in self.entrances.values()])) - if self.doors: + if self.doors and ('all' in self.settings or 'doors' in self.settings): outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( ['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', @@ -3319,33 +3388,37 @@ class Spoiler(object): self.world.fish.translate("meta", "doors", entry['exit']), '({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] != 'basic' else '') for entry in self.doors.values()])) - if self.lobbies: + if self.lobbies and ('all' in self.settings or 'doors' in self.settings): outfile.write('\n\nDungeon Lobbies:\n\n') outfile.write('\n'.join( [f"{'Player {0}: '.format(entry['player']) if self.world.players > 1 else ''}{entry['lobby_name']}: {entry['door_name']}" for entry in self.lobbies.values()])) - if self.doorTypes: + if self.doorTypes and ('all' in self.settings or 'doors' in self.settings): # doorNames: For some reason these come in combined, somehow need to split on the thing to translate # doorTypes: Small Key, Bombable, Bonkable outfile.write('\n\nDoor Types:\n\n') outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta", "doors", entry['doorNames']), self.world.fish.translate("meta", "doorTypes", entry['type'])) for entry in self.doorTypes.values()])) + # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nLocations:\n\n') - outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) for grouping in self.locations.values() for (location, item) in grouping.items()])) + outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) + for grouping in self.locations.values() for (location, item) in grouping.items()])) # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names - outfile.write('\n\nShops:\n\n') - outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta", "locations", shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta", "items", item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) + if 'all' in self.settings or 'shops' in self.settings: + outfile.write('\n\nShops:\n\n') + outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta", "locations", shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta", "items", item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) - for player in range(1, self.world.players + 1): - if self.world.boss_shuffle[player] != 'none': - bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses - outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') - outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) + if 'all' in self.settings or 'bosses' in self.settings: + for player in range(1, self.world.players + 1): + if self.world.boss_shuffle[player] != 'none': + bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses + outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') + outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) def extras(self, filename): # todo: conditional on enemy shuffle mode @@ -3506,7 +3579,7 @@ dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4 er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10, "district": 11} -# byte 1: LLLW WSS? (logic, mode, sword) +# byte 1: LLLW WSSB (logic, mode, sword, bombbag) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4, "hybridglitches": 5} world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} @@ -3517,12 +3590,12 @@ goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'cryst diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} -# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity) +# byte 3: SDMM PIII (shop, decouple doors, mixed travel, palettes, intensity) # keydrop now has it's own byte mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} # intensity is 3 bits (reserves 4-7 levels) -# new byte 4: TDDD PPPP (tavern shuffle, drop, pottery) +# byte 4: TDDD PPPP (tavern shuffle, drop, pottery) # dropshuffle reserves 2 bits, pottery needs 4) drop_shuffle_mode = {'none': 0, 'keys': 1, 'underworld': 2} pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7, @@ -3531,17 +3604,17 @@ pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'ca # byte 5: SCCC CTTX (self-loop doors, crystals gt, ctr2, experimental) counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} -# byte 6: ?CCC CPAA (crystals ganon, pyramid, access +# byte 6: LCCC CPAA (shuffle links, crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) +# byte 7: B?MC DDPP (big, ?, maps, compass, door_type, prize shuffle) door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} -enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} +prizeshuffle_mode = {'none': 0, 'dungeon': 1, 'wild': 3} -# byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) -# potshuffle decprecated, now unused +# byte 8: HHHD DPEE (enemy_health, enemy_dmg, potshuffle, enemies) e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} +enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} # byte 9: RRAA ABBB (restrict boss mode, algorithm, boss shuffle) rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2} @@ -3560,9 +3633,9 @@ flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} # byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} -keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} # reserved 8 modes? +bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} -bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} +keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} # additions # byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) @@ -3584,7 +3657,7 @@ class Settings(object): (dr_mode[w.doorShuffle[p]] << 5) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) - | (sword_mode[w.swords[p]] << 1), + | (sword_mode[w.swords[p]] << 1) | (0x1 if w.bombbag[p] else 0), (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), @@ -3600,15 +3673,15 @@ class Settings(object): | ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) | (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0), - ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) + (0x80 if w.shufflelinks[p] else 0) | ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) | (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]], (0x80 if w.bigkeyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), + | (door_type_mode[w.door_type_mode[p]] << 2) | prizeshuffle_mode[w.prizeshuffle[p]], - (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) - | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), + (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) + | (0x4 if w.potshuffle[p] else 0) | (enemy_mode[w.enemy_shuffle[p]]), (rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]), @@ -3642,15 +3715,19 @@ class Settings(object): args.shuffle[p] = r(er_mode)[settings[0] & 0x1F] args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xE0) >> 5] + args.logic[p] = r(logic_mode)[(settings[1] & 0xE0) >> 5] args.mode[p] = r(world_mode)[(settings[1] & 0x18) >> 3] args.swords[p] = r(sword_mode)[(settings[1] & 0x6) >> 1] + args.bombbag[p] = True if settings[1] & 0x1 else False + args.difficulty[p] = r(diff_mode)[(settings[2] & 0x18) >> 3] args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1] args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5] args.accessibility[p] = r(access_mode)[settings[6] & 0x3] # args.retro[p] = True if settings[1] & 0x01 else False args.hints[p] = True if settings[2] & 0x01 else False + args.shopsanity[p] = True if settings[3] & 0x80 else False args.decoupledoors[p] = True if settings[3] & 0x40 else False args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] @@ -3668,22 +3745,21 @@ class Settings(object): args.crystals_gt[p] = "random" if cgt == 8 else cgt args.experimental[p] = True if settings[5] & 0x1 else False + args.shufflelinks[p] = True if settings[6] & 0x80 else False cgan = (settings[6] & 0x78) >> 3 args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.openpyramid[p] = True if settings[6] & 0x4 else False args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False - # args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] - args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] + args.prizeshuffle[p] = r(prizeshuffle_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[8] & 0x18) >> 3] args.shufflepots[p] = True if settings[8] & 0x4 else False - args.bombbag[p] = True if settings[8] & 0x2 else False - args.shufflelinks[p] = True if settings[8] & 0x1 else False + args.shuffleenemies[p] = r(enemy_mode)[settings[8] & 0x3] if len(settings) > 9: args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6] diff --git a/CLI.py b/CLI.py index 91c6a209..dfe2a680 100644 --- a/CLI.py +++ b/CLI.py @@ -135,7 +135,7 @@ def parse_cli(argv, no_defaults=False): 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', - 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', + 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_max_difference', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', @@ -202,7 +202,7 @@ def parse_settings(): "bonk_drops": False, "shuffle": "vanilla", "shufflelinks": False, - "shuffletavern": False, + "shuffletavern": True, "overworld_map": "default", "take_any": "none", "pseudoboots": False, @@ -223,6 +223,7 @@ def parse_settings(): "compassshuffle": False, "keyshuffle": "none", "bigkeyshuffle": False, + "prizeshuffle": "none", "keysanity": False, "door_shuffle": "vanilla", "intensity": 3, @@ -236,7 +237,7 @@ def parse_settings(): "mixed_travel": "prevent", "standardize_palettes": "standardize", 'aga_randomness': True, - + "triforce_pool": 0, "triforce_goal": 0, "triforce_pool_min": 0, @@ -267,10 +268,9 @@ def parse_settings(): "msu_resume": False, "collection_rate": False, - # Spoiler defaults to TRUE + 'spoiler': 'full', # Playthrough defaults to TRUE # ROM defaults to TRUE - "create_spoiler": True, "calc_playthrough": True, "create_rom": True, "bps": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index 9ca2227a..074e71d4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1537,7 +1537,7 @@ def check_entrance_fixes(world, player): 'Turtle Rock': 'tr', 'Ganons Tower': 'gt', } - if world.mode[player] == 'inverted': + if world.is_atgt_swapped(player): del checks['Ganons Tower'] for ent_name, key in checks.items(): entrance = world.get_entrance(ent_name, player) @@ -1629,7 +1629,7 @@ def refine_hints(dungeon_builders): for name, builder in dungeon_builders.items(): for region in builder.master_sector.regions: for location in region.locations: - if not location.event and '- Boss' not in location.name and '- Prize' not in location.name and location.name != 'Sanctuary': + if not location.event and '- Boss' not in location.name and not location.prize and location.name != 'Sanctuary': if location.type == LocationType.Pot and location.pot: hint_text = ('under a block' if location.pot.flags & PotFlags.Block else 'in a pot') location.hint_text = f'{hint_text} {dungeon_hints[name]}' @@ -2696,6 +2696,8 @@ def calc_used_dungeon_items(builder, world, player): base += 1 if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): base += 1 + if world.prizeshuffle[player] == 'dungeon' and builder.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: + base += 1 return base @@ -3351,7 +3353,7 @@ def remove_pair_type_if_present(door, world, player): def find_inaccessible_regions(world, player): world.inaccessible_regions[player] = [] start_regions = ['Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop'] - start_regions.append('Sanctuary' if world.mode[player] != 'inverted' else 'Dark Sanctuary Hint') + start_regions.append('Sanctuary' if not world.is_dark_chapel_start(player) else 'Dark Sanctuary Hint') regs = convert_regions(start_regions, world, player) if all(all(not e.connected_region for e in r.exits) for r in regs): # if attempting to find inaccessible regions before any connections made above, assume eventual access to Pyramid S&Q @@ -3363,7 +3365,7 @@ def find_inaccessible_regions(world, player): while len(queue) > 0: next_region = queue.popleft() visited_regions.add(next_region) - if world.mode[player] == 'inverted' and next_region.name == 'Dark Sanctuary Hint': # special spawn point in cave + if world.is_dark_chapel_start(player) and next_region.name == 'Dark Sanctuary Hint': # special spawn point in cave for ent in next_region.entrances: parent = ent.parent_region if parent and parent.type is not RegionType.Dungeon and parent not in queue and parent not in visited_regions: @@ -3397,7 +3399,7 @@ def find_accessible_entrances(world, player, builder): start_regions = ['Hyrule Castle Courtyard'] else: start_regions = ['Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop'] - start_regions.append('Sanctuary' if world.mode[player] != 'inverted' else 'Dark Sanctuary Hint') + start_regions.append('Sanctuary' if not world.is_dark_chapel_start(player) else 'Dark Sanctuary Hint') start_regions.append('Pyramid Area' if not world.is_tile_swapped(0x1b, player) else 'Hyrule Castle Ledge') regs = convert_regions(start_regions, world, player) @@ -3405,7 +3407,7 @@ def find_accessible_entrances(world, player, builder): visited_entrances = [] # Add Sanctuary as an additional entrance in open mode, since you can save and quit to there - if world.mode[player] == 'open' and world.get_region('Sanctuary', player).dungeon.name == builder.name and 'Sanctuary' not in entrances: + if not world.is_dark_chapel_start(player) and world.get_region('Sanctuary', player).dungeon.name == builder.name and 'Sanctuary' not in entrances: entrances.append('Sanctuary') visited_entrances.append('Sanctuary') regs.remove(world.get_region('Sanctuary', player)) @@ -3630,13 +3632,17 @@ logical_connections = [ ('Hyrule Castle Throne Room Tapestry', 'Hyrule Castle Behind Tapestry'), ('Hyrule Castle Tapestry Backwards', 'Hyrule Castle Throne Room'), ('Sewers Secret Room Push Block', 'Sewers Secret Room Blocked Path'), + ('Eastern Hint Tile Push Block', 'Eastern Hint Tile'), ('Eastern Map Balcony Hook Path', 'Eastern Map Room'), ('Eastern Map Room Drop Down', 'Eastern Map Balcony'), + ('Eastern Palace Boss', 'Eastern Boss Spoils'), + ('Desert Main Lobby Left Path', 'Desert Left Alcove'), ('Desert Main Lobby Right Path', 'Desert Right Alcove'), ('Desert Left Alcove Path', 'Desert Main Lobby'), ('Desert Right Alcove Path', 'Desert Main Lobby'), + ('Desert Palace Boss', 'Desert Boss Spoils'), ('Hera Lobby to Front Barrier - Blue', 'Hera Front'), ('Hera Front to Lobby Barrier - Blue', 'Hera Lobby'), @@ -3666,6 +3672,7 @@ logical_connections = [ ('Hera Big Chest Hook Path', 'Hera Big Chest Landing'), ('Hera Big Chest Landing Exit', 'Hera 4F'), ('Hera 5F Orange Path', 'Hera 5F Pot Block'), + ('Tower of Hera Boss', 'Hera Boss Spoils'), ('PoD Pit Room Block Path N', 'PoD Pit Room Blocked'), ('PoD Pit Room Block Path S', 'PoD Pit Room'), @@ -3717,6 +3724,8 @@ logical_connections = [ ('PoD Dark Pegs Middle Ranged Crystal Exit', 'PoD Dark Pegs Middle'), ('PoD Dark Pegs Middle to Left Bypass', 'PoD Dark Pegs Left'), ('PoD Dark Pegs Left Ranged Crystal Exit', 'PoD Dark Pegs Left'), + ('Palace of Darkness Boss', 'PoD Boss Spoils'), + ('Swamp Lobby Moat', 'Swamp Entrance'), ('Swamp Entrance Moat', 'Swamp Lobby'), ('Swamp Trench 1 Approach Dry', 'Swamp Trench 1 Nexus'), @@ -3759,12 +3768,15 @@ logical_connections = [ ('Swamp Drain Right Switch', 'Swamp Drain Left'), ('Swamp Flooded Spot Ladder', 'Swamp Flooded Room'), ('Swamp Flooded Room Ladder', 'Swamp Flooded Spot'), + ('Swamp Palace Boss', 'Swamp Boss Spoils'), ('Skull Pot Circle Star Path', 'Skull Map Room'), ('Skull Big Chest Hookpath', 'Skull 1 Lobby'), ('Skull Back Drop Star Path', 'Skull Small Hall'), ('Skull 2 West Lobby Pits', 'Skull 2 West Lobby Ledge'), ('Skull 2 West Lobby Ledge Pits', 'Skull 2 West Lobby'), + ('Skull Woods Boss', 'Skull Boss Spoils'), + ('Thieves Rail Ledge Drop Down', 'Thieves BK Corner'), ('Thieves Hellway Orange Barrier', 'Thieves Hellway S Crystal'), ('Thieves Hellway Crystal Orange Barrier', 'Thieves Hellway'), @@ -3780,6 +3792,7 @@ logical_connections = [ ('Thieves Conveyor Block Path', 'Thieves Conveyor Bridge'), ("Thieves Blind's Cell Door", "Thieves Blind's Cell Interior"), ("Thieves Blind's Cell Exit", "Thieves Blind's Cell"), + ('Thieves Town Boss', 'Thieves Boss Spoils'), ('Ice Cross Bottom Push Block Left', 'Ice Floor Switch'), ('Ice Cross Right Push Block Top', 'Ice Bomb Drop'), @@ -3800,6 +3813,7 @@ logical_connections = [ ('Ice Big Chest Landing Push Blocks', 'Ice Big Chest View'), ('Ice Refill to Crystal', 'Ice Refill - Crystal'), ('Ice Refill Crystal Exit', 'Ice Refill'), + ('Ice Palace Boss', 'Ice Boss Spoils'), ('Mire Lobby Gap', 'Mire Post-Gap'), ('Mire Post-Gap Gap', 'Mire Lobby'), @@ -3835,6 +3849,7 @@ logical_connections = [ ('Mire South Fish Blue Barrier', 'Mire Fishbone'), ('Mire Fishbone Blue Barrier', 'Mire South Fish'), ('Mire Fishbone Blue Barrier Bypass', 'Mire South Fish'), + ('Misery Mire Boss', 'Mire Boss Spoils'), ('TR Main Lobby Gap', 'TR Lobby Ledge'), ('TR Lobby Ledge Gap', 'TR Main Lobby'), @@ -3882,6 +3897,7 @@ logical_connections = [ ('TR Crystal Maze End Ranged Crystal Exit', 'TR Crystal Maze End'), ('TR Final Abyss Balcony Path', 'TR Final Abyss Ledge'), ('TR Final Abyss Ledge Path', 'TR Final Abyss Balcony'), + ('Turtle Rock Boss', 'TR Boss Spoils'), ('GT Blocked Stairs Block Path', 'GT Big Chest'), ('GT Speed Torch South Path', 'GT Speed Torch'), diff --git a/Doors.py b/Doors.py index 4c9611fc..c5519d13 100644 --- a/Doors.py +++ b/Doors.py @@ -188,6 +188,7 @@ def create_doors(world, player): create_door(player, 'Eastern Duo Eyegores SE', Intr).dir(So, 0xd8, Right, High).pos(1), create_door(player, 'Eastern Duo Eyegores NE', Nrml).dir(No, 0xd8, Right, High).trap(0x4).pos(0), create_door(player, 'Eastern Boss SE', Nrml).dir(So, 0xc8, Right, High).no_exit().trap(0x4).pos(0), + create_door(player, 'Eastern Palace Boss', Lgcl), # Desert Palace create_door(player, 'Desert Main Lobby S', Nrml).dir(So, 0x84, Mid, High).pos(0).portal(Z, 0x22), @@ -251,6 +252,7 @@ def create_doors(world, player): create_door(player, 'Desert Wall Slide SE', Intr).dir(So, 0x43, Right, High).small_key().pos(1), create_door(player, 'Desert Wall Slide NW', Nrml).dir(No, 0x43, Left, High).big_key().pos(0).no_entrance(), create_door(player, 'Desert Boss SW', Nrml).dir(So, 0x33, Left, High).no_exit().trap(0x4).pos(0).portal(Z, 0x00), + create_door(player, 'Desert Palace Boss', Lgcl), # Hera create_door(player, 'Hera Lobby S', Nrml).dir(So, 0x77, Mid, Low).pos(0).portal(Z, 0x22, 1), @@ -316,6 +318,7 @@ def create_doors(world, player): create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0).kill(), create_door(player, 'Hera Boss Outer Hole', Hole), create_door(player, 'Hera Boss Inner Hole', Hole), + create_door(player, 'Tower of Hera Boss', Lgcl), # Castle Tower create_door(player, 'Tower Lobby S', Nrml).dir(So, 0xe0, Left, High).pos(3).portal(Z, 0x00), @@ -475,6 +478,7 @@ def create_doors(world, player): create_door(player, 'PoD Callback WS', Intr).dir(We, 0x0b, Mid, High).pos(1), create_door(player, 'PoD Callback Warp', Warp), create_door(player, 'PoD Boss SE', Nrml).dir(So, 0x5a, Right, High).no_exit().trap(0x4).pos(0), + create_door(player, 'Palace of Darkness Boss', Lgcl), create_door(player, 'Swamp Lobby S', Nrml).dir(So, 0x28, Mid, High).pos(1).portal(Z, 0x22), create_door(player, 'Swamp Lobby Moat', Lgcl), @@ -589,6 +593,7 @@ def create_doors(world, player): create_door(player, 'Swamp T SW', Intr).dir(So, 0x16, Left, High).small_key().pos(1), create_door(player, 'Swamp T NW', Nrml).dir(No, 0x16, Left, High).pos(3), create_door(player, 'Swamp Boss SW', Nrml).dir(So, 0x06, Left, High).no_exit().trap(0x4).pos(0), + create_door(player, 'Swamp Palace Boss', Lgcl), create_door(player, 'Skull 1 Lobby S', Nrml).dir(So, 0x58, Left, High).pos(4).portal(Z, 0x00), create_door(player, 'Skull 1 Lobby WS', Nrml).dir(We, 0x58, Bot, High).small_key().pos(1), @@ -640,6 +645,7 @@ def create_doors(world, player): create_door(player, 'Skull Spike Corner ES', Intr).dir(Ea, 0x39, Bot, High).small_key().pos(1), create_door(player, 'Skull Final Drop WS', Intr).dir(We, 0x39, Bot, High).small_key().pos(1), create_door(player, 'Skull Final Drop Hole', Hole), + create_door(player, 'Skull Woods Boss', Lgcl), create_door(player, 'Thieves Lobby S', Nrml).dir(So, 0xdb, Mid, High).pos(1).portal(Z, 0x22), create_door(player, 'Thieves Lobby N Edge', Open).dir(No, 0xdb, None, Low).edge(7, A, 0x10), @@ -721,6 +727,7 @@ def create_doors(world, player): create_door(player, 'Thieves Big Chest Room ES', Intr).dir(Ea, 0x44, Bot, High).small_key().pos(1), create_door(player, 'Thieves Conveyor Block WN', Intr).dir(We, 0x44, Top, High).pos(0), create_door(player, 'Thieves Trap EN', Intr).dir(Ea, 0x44, Left, Top).pos(0), + create_door(player, 'Thieves Town Boss', Lgcl), create_door(player, 'Ice Lobby SE', Nrml).dir(So, 0x0e, Right, High).pos(2).portal(X, 0x00), create_door(player, 'Ice Lobby WS', Intr).dir(We, 0x0e, Bot, High).pos(1), @@ -826,6 +833,7 @@ def create_doors(world, player): create_door(player, 'Ice Fairy Warp', Warp), create_door(player, 'Ice Antechamber NE', Nrml).dir(No, 0xce, Right, High).trap(0x4).pos(0), create_door(player, 'Ice Antechamber Hole', Hole), + create_door(player, 'Ice Palace Boss', Lgcl), create_door(player, 'Mire Lobby S', Nrml).dir(So, 0x98, Left, High).pos(0).portal(Z, 0x20), create_door(player, 'Mire Lobby Gap', Lgcl), @@ -962,6 +970,7 @@ def create_doors(world, player): create_door(player, 'Mire Antechamber Orange Barrier', Lgcl), create_door(player, 'Mire Antechamber NW', Nrml).dir(No, 0xa0, Left, High).big_key().pos(0), create_door(player, 'Mire Boss SW', Nrml).dir(So, 0x90, Left, High).no_exit().trap(0x4).pos(0), + create_door(player, 'Misery Mire Boss', Lgcl), create_door(player, 'TR Main Lobby SE', Nrml).dir(So, 0xd6, Right, High).pos(1).portal(X, 0x02), create_door(player, 'TR Lobby Ledge NE', Nrml).dir(No, 0xd6, Right, High).pos(2), @@ -1075,6 +1084,7 @@ def create_doors(world, player): create_door(player, 'TR Final Abyss Ledge Path', Lgcl), create_door(player, 'TR Final Abyss NW', Nrml).dir(No, 0xb4, Left, High).big_key().pos(0), create_door(player, 'TR Boss SW', Nrml).dir(So, 0xa4, Left, High).no_exit().trap(0x4).pos(0).portal(Z, 0x00), + create_door(player, 'Turtle Rock Boss', Lgcl), create_door(player, 'GT Lobby S', Nrml).dir(So, 0x0c, Mid, High).pos(0).portal(Z, 0x22), create_door(player, 'GT Lobby Left Down Stairs', Sprl).dir(Dn, 0x0c, 1, HTL).ss(A, 0x0f, 0x80), @@ -1506,7 +1516,7 @@ def create_doors(world, player): # static portal flags world.get_door('Sanctuary S', player).dead_end(allowPassage=True) - if world.mode[player] == 'open' and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']: + if not world.is_dark_chapel_start(player) and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']: world.get_door('Sanctuary S', player).lw_restricted = True world.get_door('Eastern Hint Tile Blocked Path SE', player).passage = False world.get_door('TR Big Chest Entrance SE', player).passage = False diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5f34fbf2..d5b0dafd 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -11,7 +11,7 @@ from typing import List from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector from BaseClasses import Hook, hook_from_door, Door -from Regions import dungeon_events, flooded_keys_reverse +from Regions import location_events, flooded_keys_reverse from Dungeons import split_region_starts from RoomData import DoorKind @@ -884,19 +884,19 @@ class ExplorationState(object): if key_checks and location not in self.found_locations: if location.forced_item and 'Small Key' in location.item.name: self.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: + if location.name not in location_events and not location.prize: self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name): self.bk_found.add(location) - if location.name in dungeon_events and location.name not in self.events: + if location.name in location_events and location.name not in self.events: if self.flooded_key_check(location): self.perform_event(location.name, key_region) if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) - if '- Prize' in location.name: + if location.prize: self.prize_received = True def flooded_key_check(self, location): @@ -1096,7 +1096,7 @@ def count_locations_exclude_big_chest(locations, world, player): def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + return loc.name in location_events or loc.prize def reserved_location(loc, world, player): @@ -1360,7 +1360,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge for name, builder in dungeon_map.items(): calc_allowance_and_dead_ends(builder, connections_tuple, world, player) - if world.mode[player] == 'open' and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']: + if not world.is_dark_chapel_start(player) and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']: sanc = find_sector('Sanctuary', candidate_sectors) if sanc: # only run if sanc if a candidate lw_builders = [] @@ -1557,13 +1557,13 @@ def define_sector_features(sectors): for sector in sectors: for region in sector.regions: for loc in region.locations: - if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: + if loc.prize or loc.name in ['Agahnim 1', 'Agahnim 2']: pass elif loc.forced_item and 'Small Key' in loc.item.name: sector.key_only_locations += 1 elif loc.forced_item and loc.forced_item.bigkey: sector.bk_provided = True - elif loc.name not in dungeon_events and not loc.forced_item: + elif loc.name not in location_events and not loc.forced_item: sector.chest_locations += 1 sector.chest_location_set.add(loc.name) if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest", @@ -1773,6 +1773,8 @@ def build_orig_location_set(dungeon_map): def requested_dungeon_items(world, player): num = 0 + if world.prizeshuffle[player] == 'dungeon': + num += 1 if not world.bigkeyshuffle[player]: num += 1 if not world.compassshuffle[player]: @@ -4055,7 +4057,7 @@ def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None): crystal_barrier = CrystalBarrier.Either # todo: backtracking from double switch with orange on-- for loc in region.locations: - if loc.name in dungeon_events: + if loc.name in location_events: found_events.add(loc.name) for d in event_doors: if loc.name == d.req_event: diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index cf0f73bc..94b83faf 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -23,7 +23,7 @@ def start(): # print diagnostics # usage: py DungeonRandomizer.py --diags if args.diags: - diags = diagnostics.output(__version__) + diags = diagnostics.output() print("\n".join(diags)) sys.exit(0) diff --git a/Dungeons.py b/Dungeons.py index 3284617e..8d72760b 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -34,16 +34,16 @@ def create_dungeons(world, player): world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] -dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], - 'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E], - 'Tower of Hera - Prize': [0x155C5, 0x1107A, 0x10B8C], - 'Palace of Darkness - Prize': [0x155B8], - 'Swamp Palace - Prize': [0x155B7], - 'Thieves\' Town - Prize': [0x155C6], - 'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B], - 'Ice Palace - Prize': [0x155BF], - 'Misery Mire - Prize': [0x155B9], - 'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} +dungeon_music_addresses = {'Eastern Palace': [0x1559A], + 'Desert Palace': [0x1559B, 0x1559C, 0x1559D, 0x1559E], + 'Tower of Hera': [0x155C5, 0x1107A, 0x10B8C], + 'Palace of Darkness': [0x155B8], + 'Swamp Palace': [0x155B7], + 'Thieves Town': [0x155C6], + 'Skull Woods': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B], + 'Ice Palace': [0x155BF], + 'Misery Mire': [0x155B9], + 'Turtle Rock': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} hyrule_castle_regions = [ 'Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Hyrule Castle East Hall', @@ -65,7 +65,7 @@ eastern_regions = [ 'Eastern Compass Room', 'Eastern Hint Tile', 'Eastern Hint Tile Blocked Path', 'Eastern Courtyard', 'Eastern Fairies', 'Eastern Map Valley', 'Eastern Dark Square', 'Eastern Dark Pots', 'Eastern Big Key', 'Eastern Darkness', 'Eastern Rupees', 'Eastern Attic Start', 'Eastern False Switches', 'Eastern Cannonball Hell', - 'Eastern Single Eyegore', 'Eastern Duo Eyegores', 'Eastern Boss', 'Eastern Portal' + 'Eastern Single Eyegore', 'Eastern Duo Eyegores', 'Eastern Boss', 'Eastern Boss Spoils', 'Eastern Portal' ] desert_regions = [ @@ -74,7 +74,7 @@ desert_regions = [ 'Desert North Hall', 'Desert Map Room', 'Desert Sandworm Corner', 'Desert Bonk Torch', 'Desert Circle of Pots', 'Desert Big Chest Room', 'Desert West Wing', 'Desert West Lobby', 'Desert Fairy Fountain', 'Desert Back Lobby', 'Desert Tiles 1', 'Desert Bridge', 'Desert Four Statues', 'Desert Beamos Hall', 'Desert Tiles 2', - 'Desert Wall Slide', 'Desert Boss', 'Desert West Portal', 'Desert South Portal', 'Desert East Portal', + 'Desert Wall Slide', 'Desert Boss', 'Desert Boss Spoils', 'Desert West Portal', 'Desert South Portal', 'Desert East Portal', 'Desert Back Portal' ] @@ -84,7 +84,7 @@ hera_regions = [ 'Hera Back - Ranged Crystal', 'Hera Basement Cage', 'Hera Basement Cage - Crystal', 'Hera Tile Room', 'Hera Tridorm', 'Hera Tridorm - Crystal', 'Hera Torches', 'Hera Beetles', 'Hera Startile Corner', 'Hera Startile Wide', 'Hera Startile Wide - Crystal', 'Hera 4F', 'Hera Big Chest Landing', 'Hera 5F', - 'Hera 5F Pot Block', 'Hera Fairies', 'Hera Boss', 'Hera Portal' + 'Hera 5F Pot Block', 'Hera Fairies', 'Hera Boss', 'Hera Boss Spoils', 'Hera Portal' ] tower_regions = [ @@ -105,7 +105,7 @@ pod_regions = [ 'PoD Bow Statue Left', 'PoD Bow Statue Left - Crystal', 'PoD Bow Statue Right', 'PoD Bow Statue Right - Ranged Crystal', 'PoD Dark Pegs Landing', 'PoD Dark Pegs Right', 'PoD Dark Pegs Middle', 'PoD Dark Pegs Left', 'PoD Dark Pegs Landing - Ranged Crystal', 'PoD Dark Pegs Middle - Ranged Crystal', 'PoD Dark Pegs Left - Ranged Crystal', 'PoD Lonely Turtle', 'PoD Turtle Party', - 'PoD Dark Alley', 'PoD Callback', 'PoD Boss', 'Palace of Darkness Portal' + 'PoD Dark Alley', 'PoD Callback', 'PoD Boss', 'PoD Boss Spoils', 'Palace of Darkness Portal' ] swamp_regions = [ @@ -119,7 +119,7 @@ swamp_regions = [ 'Swamp West Ledge', 'Swamp Barrier Ledge', 'Swamp Barrier', 'Swamp Attic', 'Swamp Push Statue', 'Swamp Shooters', 'Swamp Left Elbow', 'Swamp Right Elbow', 'Swamp Drain Left', 'Swamp Drain Right', 'Swamp Flooded Room', 'Swamp Flooded Spot', 'Swamp Basement Shallows', 'Swamp Waterfall Room', 'Swamp Refill', 'Swamp Behind Waterfall', - 'Swamp C', 'Swamp Waterway', 'Swamp I', 'Swamp T', 'Swamp Boss', 'Swamp Portal' + 'Swamp C', 'Swamp Waterway', 'Swamp I', 'Swamp T', 'Swamp Boss', 'Swamp Boss Spoils', 'Swamp Portal' ] skull_regions = [ @@ -127,7 +127,7 @@ skull_regions = [ 'Skull Pot Prison', 'Skull Compass Room', 'Skull Left Drop', 'Skull 2 East Lobby', 'Skull Big Key', 'Skull Lone Pot', 'Skull Small Hall', 'Skull Back Drop', 'Skull 2 West Lobby', 'Skull 2 West Lobby Ledge', 'Skull X Room', 'Skull 3 Lobby', 'Skull East Bridge', 'Skull West Bridge Nook', 'Skull Star Pits', - 'Skull Torch Room', 'Skull Vines', 'Skull Spike Corner', 'Skull Final Drop', 'Skull Boss', + 'Skull Torch Room', 'Skull Vines', 'Skull Spike Corner', 'Skull Final Drop', 'Skull Boss', 'Skull Boss Spoils', 'Skull 1 Portal', 'Skull 2 East Portal', 'Skull 2 West Portal', 'Skull 3 Portal' ] @@ -139,7 +139,7 @@ thieves_regions = [ 'Thieves Attic', 'Thieves Attic Hint', 'Thieves Attic Switch', 'Thieves Cricket Hall Left', 'Thieves Cricket Hall Right', 'Thieves Attic Window', 'Thieves Basement Block', 'Thieves Blocked Entry', 'Thieves Lonely Zazak', "Thieves Blind's Cell", "Thieves Blind's Cell Interior", 'Thieves Conveyor Bridge', - 'Thieves Conveyor Block', 'Thieves Big Chest Room', 'Thieves Trap', 'Thieves Town Portal' + 'Thieves Conveyor Block', 'Thieves Big Chest Room', 'Thieves Trap', 'Thieves Boss Spoils', 'Thieves Town Portal' ] ice_regions = [ @@ -152,7 +152,7 @@ ice_regions = [ 'Ice Hookshot Balcony', 'Ice Spikeball', 'Ice Lonely Freezor', 'Iced T', 'Ice Catwalk', 'Ice Many Pots', 'Ice Crystal Right', 'Ice Crystal Left', 'Ice Crystal Block', 'Ice Big Chest View', 'Ice Big Chest Landing', 'Ice Backwards Room', 'Ice Anti-Fairy', 'Ice Switch Room', 'Ice Refill', 'Ice Refill - Crystal', - 'Ice Fairy', 'Ice Antechamber', 'Ice Boss', 'Ice Portal' + 'Ice Fairy', 'Ice Antechamber', 'Ice Boss', 'Ice Boss Spoils', 'Ice Portal' ] mire_regions = [ @@ -168,7 +168,7 @@ mire_regions = [ 'Mire Key Rupees', 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left', 'Mire Crystal Top', 'Mire Shooter Rupees', 'Mire Falling Foes', 'Mire Firesnake Skip', 'Mire Antechamber', - 'Mire Boss', 'Mire Portal' + 'Mire Boss', 'Mire Boss Spoils', 'Mire Portal' ] tr_regions = [ @@ -182,7 +182,7 @@ tr_regions = [ 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', 'TR Crystaroller Middle - Ranged Crystal', 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dark Ride Ledges', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystal Maze Start', 'TR Crystal Maze Start - Crystal', 'TR Crystal Maze Interior', 'TR Crystal Maze End', - 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss Balcony', 'TR Final Abyss Ledge', 'TR Boss', + 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss Balcony', 'TR Final Abyss Ledge', 'TR Boss', 'TR Boss Spoils', 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal', 'Turtle Rock Eye Bridge Portal' ] @@ -270,7 +270,7 @@ flexible_starts = { class DungeonInfo: - def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize, midx): + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize, dungeon_idx, extra_map_idx): # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): self.free_items = free self.key_num = keys @@ -281,23 +281,24 @@ class DungeonInfo: self.key_drops = drops self.prize = prize - self.map_index = midx + self.dungeon_index = dungeon_idx + self.extra_map_index = extra_map_idx dungeon_table = { - 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None, 0xc), - 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize', 0x0), - 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize', 0x2), - 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize', 0x1), - 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None, 0xb), - 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize', 0x3), - 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize', 0x9), - 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize', 0x4), - 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize", 0x6), - 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize', 0x8), - 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize', 0x7), - 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize', 0x5), - 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None, 0xa), + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None, 0x00, 0x00), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, [0x1209D, 0x53F3A, 0x53F3B, 0x180052, 0x180070, 0x186FE2], 0x04, None), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, [0x1209E, 0x53F3C, 0x53F3D, 0x180053, 0x180072, 0x186FE3], 0x06, 0x12), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, [0x120A5, 0x53F4A, 0x53F4B, 0x18005A, 0x180071, 0x186FEA], 0x14, None), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None, 0x08, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, [0x120A1, 0x53F42, 0x53F43, 0x180056, 0x180073, 0x186FE6], 0x0C, None), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, [0x120A0, 0x53F40, 0x53F41, 0x180055, 0x180079, 0x186FE5], 0x0A, None), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, [0x120A3, 0x53F46, 0x53F47, 0x180058, 0x180074, 0x186FE8], 0x10, 0x20), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, [0x120A6, 0x53F4C, 0x53F4D, 0x18005B, 0x180076, 0x186FEB], 0x16, None), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, [0x120A4, 0x53F48, 0x53F49, 0x180059, 0x180078, 0x186FE9], 0x12, None), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, [0x120A2, 0x53F44, 0x53F45, 0x180057, 0x180077, 0x186FE7], 0x0E, None), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, [0x120A7, 0x53F4E, 0x53F4F, 0x18005C, 0x180075, 0x186FEC], 0x18, 0x3E), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None, 0x1A, None), } diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index c86ca335..943609ce 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -316,7 +316,7 @@ Library: near books Potion Shop: near potions Spike Cave: beyond spikes Mimic Cave: in a cave of mimicry -Chest Game: as a prize +Chest Game: as a game reward Chicken House: near poultry Aginah's Cave: with Aginah Ice Rod Cave: in a frozen cave diff --git a/EntranceShuffle.py b/EntranceShuffle.py index c374ec15..e69de29b 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,2806 +0,0 @@ -import logging -from collections import defaultdict, OrderedDict -import RaceRandom as random -from BaseClasses import CollectionState, RegionType -from OverworldShuffle import build_accessible_region_list -from DoorShuffle import find_inaccessible_regions -from OWEdges import OWTileRegions -from Utils import stack_size3a - -entrance_pool = list() -exit_pool = list() -entrance_exits = list() -ignore_pool = True -suppress_spoiler = True - -def link_entrances(world, player): - invFlag = world.mode[player] == 'inverted' - - global entrance_pool, exit_pool, ignore_pool, suppress_spoiler, entrance_exits - entrance_exits = list() - ignore_pool = False - suppress_spoiler = True - links_house = False - entrance_pool = Entrance_Pool_Base.copy() - exit_pool = Exit_Pool_Base.copy() - drop_connections = default_drop_connections.copy() - dropexit_connections = default_dropexit_connections.copy() - - Dungeon_Exits = LW_Dungeon_Exits + DW_Mid_Dungeon_Exits + DW_Late_Dungeon_Exits - Cave_Exits = Cave_Exits_Base.copy() - Old_Man_House = Old_Man_House_Base.copy() - Cave_Three_Exits = Cave_Three_Exits_Base.copy() - - from OverworldShuffle import build_sectors - if not world.owsectors[player] and world.shuffle[player] != 'vanilla': - world.owsectors[player] = build_sectors(world, player) - - # modifications to lists - if not world.is_tile_swapped(0x1b, player): - drop_connections.append(tuple(('Pyramid Hole', 'Pyramid'))) - dropexit_connections.append(tuple(('Pyramid Entrance', 'Pyramid Exit'))) - else: - entrance_pool.remove('Pyramid Hole') - entrance_pool.append('Inverted Pyramid Hole') - entrance_pool.remove('Pyramid Entrance') - entrance_pool.append('Inverted Pyramid Entrance') - drop_connections.append(tuple(('Inverted Pyramid Hole', 'Pyramid'))) - dropexit_connections.append(tuple(('Inverted Pyramid Entrance', 'Pyramid Exit'))) - - unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) - Cave_Exits.extend(Cave_Exits_Directional) - - # setup mandatory connections - for exitname, regionname in mandatory_connections: - connect_simple(world, exitname, regionname, player) - - connect_simple(world, 'Tavern North', 'Tavern', player) - - suppress_spoiler = False - connect_custom(world, player) - suppress_spoiler = True - - # if we do not shuffle, set default connections - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - for entrancename, exitname in (default_connections + default_pot_connections + - default_takeany_connections + drop_connections + default_item_connections + default_shop_connections): - connect_logical(world, entrancename, exitname, player, exitname.endswith(' Exit')) - for entrancename, exitname in default_connector_connections + dropexit_connections: - connect_logical(world, entrancename, exitname, player, True) - if invFlag: - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) - if world.is_bombshop_start(player): - world.get_entrance('Big Bomb Shop Exit', player).connect(world.get_entrance('Big Bomb Shop', player).parent_region) - - ignore_pool = False - - # dungeon entrance shuffle - if world.shuffle[player] == 'vanilla': - for entrancename, exitname in default_dungeon_connections: - connect_logical(world, entrancename, exitname, player, True) - for entrancename, exitname in default_skulldrop_connections: - connect_logical(world, entrancename, exitname, player, False) - - if world.is_atgt_swapped(player): - for entrancename, exitname in inverted_default_dungeon_connections: - connect_logical(world, entrancename, exitname, player, True) - else: - for entrancename, exitname in open_default_dungeon_connections: - connect_logical(world, entrancename, exitname, player, True) - elif world.shuffle[player] == 'dungeonssimple': - suppress_spoiler = False - simple_shuffle_dungeons(world, player) - elif world.shuffle[player] == 'dungeonsfull': - suppress_spoiler = False - full_shuffle_dungeons(world, Dungeon_Exits, player) - elif world.shuffle[player] == 'simple': - suppress_spoiler = False - simple_shuffle_dungeons(world, player) - - # shuffle dropdowns - scramble_holes(world, player) - - # list modification - lw_wdm_entrances = ['Old Man Cave (West)', 'Death Mountain Return Cave (West)', - 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)'] - lw_edm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Spiral Cave', - 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)'] - ddm_entrances = ['Dark Death Mountain Fairy', 'Spike Cave'] - - caves = list(Cave_Exits) - three_exit_caves = list(Cave_Three_Exits) - - Two_Door_Caves_Directional = [('Bumper Cave (Bottom)', 'Bumper Cave (Top)')] - Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)')] - if not world.is_tile_swapped(0x05, player): - Two_Door_Caves_Directional.append(tuple({'Hookshot Cave', 'Hookshot Cave Back Entrance'})) - else: - Two_Door_Caves.append(tuple({'Hookshot Cave', 'Hookshot Cave Back Entrance'})) - if not world.is_tile_swapped(0x28, player): - Two_Door_Caves.append(tuple({'Two Brothers House (East)', 'Two Brothers House (West)'})) - else: - Two_Door_Caves_Directional.append(tuple({'Two Brothers House (East)', 'Two Brothers House (West)'})) - - # shuffle all 2 entrance caves as pairs as a start - # start with the ones that need to be directed - two_door_caves = list(Two_Door_Caves_Directional) - random.shuffle(two_door_caves) - random.shuffle(caves) - while two_door_caves: - entrance1, entrance2 = two_door_caves.pop() - exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) - - # shuffle remaining 2 entrance cave pairs - two_door_caves = list(Two_Door_Caves) - random.shuffle(two_door_caves) - while two_door_caves: - entrance1, entrance2 = two_door_caves.pop() - exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) - - # shuffle LW DM entrances - caves.extend(list(Old_Man_House)) - caves.extend(list(three_exit_caves)) - - candidates = [e for e in lw_wdm_entrances if e != 'Old Man House (Bottom)'] - random.shuffle(candidates) - old_man_exit = candidates.pop() - lw_wdm_entrances.remove(old_man_exit) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world - candidates = lw_wdm_entrances + lw_edm_entrances - random.shuffle(candidates) - old_man_entrance = candidates.pop() - if old_man_entrance in lw_wdm_entrances: - lw_wdm_entrances.remove(old_man_entrance) - elif old_man_entrance in lw_edm_entrances: - lw_edm_entrances.remove(old_man_entrance) - else: - random.shuffle(lw_wdm_entrances) - old_man_entrance = lw_wdm_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - # connect remaining LW DM entrances - if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world - connect_caves(world, lw_wdm_entrances + lw_edm_entrances, [], caves, player) - else: - # place Old Man House in WDM if not flipped - if not world.is_tile_swapped(0x03, player): - connect_caves(world, lw_wdm_entrances, [], list(Old_Man_House), player) - else: - connect_caves(world, lw_edm_entrances, [], list(Old_Man_House), player) - caves.remove(Old_Man_House[0]) - - i = 0 - c = 0 - while i != len(lw_wdm_entrances): - random.shuffle(caves) - i = 0 - c = 0 - while i < len(lw_wdm_entrances): - i += len(caves[c]) - c += 1 - - connect_caves(world, lw_wdm_entrances, [], caves[0:c], player) - connect_caves(world, lw_edm_entrances, [], caves[c:], player) - - if invFlag: - # place dark sanc - place_dark_sanc(world, player) - - # place links house - links_house = place_links_house(world, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - - # junk fill inaccessible regions - # TODO: Should be obsolete someday when OWR rebalances the shuffle to prevent unreachable regions - junk_fill_inaccessible(world, player) - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = list(entrance_pool) - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - bomb_shop = random.choice(bomb_shop_doors) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'restricted': - suppress_spoiler = False - simple_shuffle_dungeons(world, player) - - # shuffle holes - scramble_holes(world, player) - - # place dark sanc - if invFlag: - place_dark_sanc(world, player) - - # place links house - links_house = place_links_house(world, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - - # determine pools - lw_entrances = list() - dw_entrances = list() - caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) - for e in entrance_pool: - if world.mode[player] == 'standard' and e == 'Bonk Fairy (Light)': - continue - region = world.get_entrance(e, player).parent_region - if region.type == RegionType.LightWorld: - lw_entrances.append(e) - else: - dw_entrances.append(e) - - # place connectors in inaccessible regions - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) - - # place old man, has limited options - place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = list(entrance_pool) - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - bomb_shop = random.choice(bomb_shop_doors) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # shuffle connectors - lw_entrances = [e for e in lw_entrances if e in entrance_pool] - dw_entrances = [e for e in dw_entrances if e in entrance_pool] - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'full': - suppress_spoiler = False - skull_woods_shuffle(world, player) - - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) - - if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - caves.append(tuple(random.sample(['Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 2))) - else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) - - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', 'Ganons Tower Exit', player) - else: - caves.append('Ganons Tower Exit') - - # place dark sanc - if invFlag: - place_dark_sanc(world, player, list(zip(*drop_connections + dropexit_connections))[0]) - - # place links house - links_house = place_links_house(world, player, list(zip(*drop_connections + dropexit_connections))[0]) - - # determine pools - lw_entrances = list() - dw_entrances = list() - for e in entrance_pool: - if world.mode[player] == 'standard' and e == 'Bonk Fairy (Light)': - continue - if e not in list(zip(*drop_connections + dropexit_connections))[0]: - region = world.get_entrance(e, player).parent_region - if region.type == RegionType.LightWorld: - lw_entrances.append(e) - else: - dw_entrances.append(e) - - # place connectors in inaccessible regions - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, list(zip(*drop_connections + dropexit_connections))[0]) - - # place old man, has limited options - place_old_man(world, lw_entrances if not invFlag else dw_entrances, player, list(zip(*drop_connections + dropexit_connections))[0]) - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = [e for e in entrance_pool if e not in list(zip(*drop_connections + dropexit_connections))[0]] - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in bomb_shop_doors if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - bomb_shop = random.choice(bomb_shop_doors) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # shuffle connectors - lw_entrances = [e for e in lw_entrances if e in entrance_pool] - dw_entrances = [e for e in dw_entrances if e in entrance_pool] - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # shuffle holes - scramble_holes(world, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'lite': - for entrancename, exitname in (default_connections + - ([] if world.shopsanity[player] else default_shop_connections) + - ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections) + - ([] if world.take_any[player] == 'fixed' else default_takeany_connections)): - connect_logical(world, entrancename, exitname, player, False) - if invFlag: - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) - - suppress_spoiler = False - - # shuffle dungeons - skull_woods_shuffle(world, player) - - # build dungeon lists - lw_dungeons = LW_Dungeon_Exits.copy() - dw_dungeons = DW_Late_Dungeon_Exits.copy() - - if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - lw_dungeons.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) - else: - lw_dungeons.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)'))) - - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', 'Ganons Tower Exit', player) - else: - dw_dungeons.append('Ganons Tower Exit') - - unbias_dungeons(lw_dungeons) - unbias_dungeons(dw_dungeons) - - # shuffle dropdowns - scramble_holes(world, player) - - # place links house - links_house = place_links_house(world, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - - # determine pools - Cave_Base = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) - lw_entrances = list() - dw_entrances = list() - for e in entrance_pool: - region = world.get_entrance(e, player).parent_region - if region.type == RegionType.LightWorld: - lw_entrances.append(e) - else: - dw_entrances.append(e) - - # place connectors in inaccessible regions - caves = Cave_Base + (dw_dungeons if not invFlag else lw_dungeons) - connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (dw_entrances if not invFlag else lw_entrances)] - connect_inaccessible_regions(world, [], connector_entrances, caves, player) - if invFlag: - lw_dungeons = [e for e in lw_dungeons if e in caves] - else: - dw_dungeons = [e for e in dw_dungeons if e in caves] - - caves = [e for e in caves if e not in (dw_dungeons if not invFlag else lw_dungeons)] + (lw_dungeons if not invFlag else dw_dungeons) - connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (lw_entrances if not invFlag else dw_entrances)] - connect_inaccessible_regions(world, connector_entrances, [], caves, player) - if not invFlag: - lw_dungeons = [e for e in lw_dungeons if e in caves] - else: - dw_dungeons = [e for e in dw_dungeons if e in caves] - - caves = [e for e in caves if e not in (lw_dungeons if not invFlag else dw_dungeons)] + DW_Mid_Dungeon_Exits - - # place old man, has limited options - lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] - dw_entrances = [e for e in dw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] - place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) - - # shuffle remaining connectors - lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] - dw_entrances = [e for e in dw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] - connect_caves(world, lw_entrances, [], lw_dungeons, player) - connect_caves(world, [], dw_entrances, dw_dungeons, player) - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = list(entrance_pool) - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - bomb_shop = random.choice(bomb_shop_doors) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'lean': - for entrancename, exitname in (default_connections + - ([] if world.shopsanity[player] else default_shop_connections) + - ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections) + - ([] if world.take_any[player] == 'fixed' else default_takeany_connections)): - connect_logical(world, entrancename, exitname, player, False) - if invFlag: - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) - - suppress_spoiler = False - - # shuffle dungeons - skull_woods_shuffle(world, player) - - if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) - else: - Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)'))) - - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', 'Ganons Tower Exit', player) - else: - Dungeon_Exits.append('Ganons Tower Exit') - - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) - - # shuffle dropdowns - scramble_holes(world, player) - - # place links house - links_house = place_links_house(world, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - - # place connectors in inaccessible regions - connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in entrance_pool] - connect_inaccessible_regions(world, connector_entrances, [], caves, player) - - # place old man, has limited options - connector_entrances = [e for e in connector_entrances if e in entrance_pool] - place_old_man(world, list(connector_entrances), player) - - # shuffle remaining connectors - connector_entrances = [e for e in connector_entrances if e in entrance_pool] - connect_caves(world, connector_entrances, [], caves, player) - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = list(entrance_pool) - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - bomb_shop = random.choice(bomb_shop_doors) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'crossed': - suppress_spoiler = False - skull_woods_shuffle(world, player) - - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) - - if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - caves.append(tuple(random.sample(['Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 2))) - else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) - - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', 'Ganons Tower Exit', player) - else: - caves.append('Ganons Tower Exit') - - # shuffle holes - scramble_holes(world, player) - - # place dark sanc - if invFlag: - place_dark_sanc(world, player) - - # place links house - links_house = place_links_house(world, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - - # place connectors in inaccessible regions - pool = list(entrance_pool) - if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in pool: - pool.remove('Bonk Fairy (Light)') - connect_inaccessible_regions(world, pool, [], caves, player) - - # place old man, has limited options - pool = [e for e in pool if e in entrance_pool] - place_old_man(world, pool, player) - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = list(entrance_pool) - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - bomb_shop = random.choice(bomb_shop_doors) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # shuffle connectors - pool = [e for e in pool if e in entrance_pool] - connect_caves(world, pool, [], caves, player) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'insanity': - # beware ye who enter here - suppress_spoiler = False - - # list preparation - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House + \ - ['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', - 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', - 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] - - if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append('Hyrule Castle Secret Entrance') - caves.append('Hyrule Castle Secret Entrance Exit') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance' if not world.is_tile_swapped(0x1b, player) else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole' if not world.is_tile_swapped(0x1b, player) else 'Inverted Pyramid Hole', 'Pyramid', player) - else: - caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) - hole_entrances.append('Pyramid Hole' if not world.is_tile_swapped(0x1b, player) else 'Inverted Pyramid Hole') - hole_targets.append('Pyramid') - - # shuffle holes - random.shuffle(hole_entrances) - random.shuffle(hole_targets) - for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) - - # place dark sanc - if invFlag: - place_dark_sanc(world, player) - - # place links house - links_house = place_links_house(world, player) - - # place blacksmith, place sanc exit first for additional blacksmith candidates - doors = list(entrance_pool) - random.shuffle(doors) - door = doors.pop() - connect_entrance(world, door, 'Sanctuary Exit', player, False) - doors = [e for e in doors if e not in entrance_exits] - door = doors.pop() - connect_exit(world, 'Sanctuary Exit', door, player, False) - caves.remove('Sanctuary Exit') - place_blacksmith(world, links_house, player) - - # place connectors in inaccessible regions - pool = list(entrance_pool) - if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in pool: - pool.remove('Bonk Fairy (Light)') - connect_inaccessible_regions(world, pool, [], caves, player) - - # place old man, has limited options - pool = [e for e in pool if e in entrance_pool] - place_old_man(world, pool, player) - caves.append('Old Man Cave Exit (West)') - - # place bomb shop, has limited options - if not world.is_bombshop_start(player): - bomb_shop_doors = list(entrance_pool) - if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - if world.is_tile_swapped(0x03, player): - bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - pool.remove(bomb_shop) - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - - # shuffle connectors - doors = list(entrance_pool) - if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in doors: - doors.remove('Bonk Fairy (Light)') - exit_doors = [e for e in entrance_pool if e not in entrance_exits] - random.shuffle(doors) - random.shuffle(exit_doors) - for cave in caves: - if isinstance(cave, str): - cave = (cave,) - for exit in cave: - connect_exit(world, exit, exit_doors.pop(), player, False) - connect_entrance(world, doors.pop(), exit, player, False) - - # place remaining doors - connect_doors(world, list(entrance_pool), list(exit_pool), player) - else: - raise NotImplementedError('Shuffling not supported yet') - - # ensure Houlihan exits where Links House does - # TODO: Plando should overrule this - if not links_house: - for links_house in world.get_entrance('Links House Exit' if not world.is_bombshop_start(player) else 'Big Bomb Shop Exit', player).connected_region.exits: - if links_house.connected_region and links_house.connected_region.name == ('Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop'): - links_house = links_house.name - break - connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) - ignore_pool = True - - # check for swamp palace fix - if not (world.get_entrance('Dam', player).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', player).connected_region.name in ['Dam', 'Swamp Portal']): - world.swamp_patch_required[player] = True - - # check for potion shop location - if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': - world.powder_patch_required[player] = True - - # check for ganon location - if world.get_entrance('Pyramid Hole' if not world.is_tile_swapped(0x1b, player) else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': - world.ganon_at_pyramid[player] = False - - # check for Ganon's Tower location - if world.get_entrance('Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', player).connected_region.name != 'Ganons Tower Portal': - world.ganonstower_vanilla[player] = False - - -def connect_custom(world, player): - if hasattr(world, 'custom_entrances') and world.custom_entrances[player]: - for exit_name, region_name in world.custom_entrances[player]: - # doesn't actually change addresses - connect_simple(world, exit_name, region_name, player) - # this needs to remove custom connections from the pool - - -def connect_simple(world, exitname, regionname, player): - world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) - - -def connect_logical(world, entrancename, exitname, player, isTwoWay = False): - if not ignore_pool: - logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) - assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename - assert exitname in exit_pool, 'Exit not in pool: ' + exitname - - try: - region = world.get_region(exitname, player) - exit = None - except RuntimeError: - exit = world.get_entrance(exitname, player) - region = exit.parent_region - - connect_simple(world, entrancename, region.name, player) - if isTwoWay: - region = world.get_entrance(entrancename, player).parent_region - connect_simple(world, exitname, region.name, player) - - if not ignore_pool: - entrance_pool.remove(entrancename) - exit_pool.remove(exitname) - - -def connect_entrance(world, entrancename, exitname, player, mark_two_way=True): - if not ignore_pool: - logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) - assert entrancename in entrance_pool, f'Entrance not in pool: {entrancename}' - if mark_two_way: - assert exitname in exit_pool, f'Exit not in pool: {exitname}' - - entrance = world.get_entrance(entrancename, player) - # check if we got an entrance or a region to connect to - try: - region = world.get_region(exitname, player) - exit = None - except RuntimeError: - exit = world.get_entrance(exitname, player) - region = exit.parent_region - - # if this was already connected somewhere, remove the backreference - if entrance.connected_region is not None: - entrance.connected_region.entrances.remove(entrance) - - target = exit_ids[exit.name][0] if exit is not None else exit_ids.get(region.name, None) - addresses = door_addresses[entrance.name][0] - - entrance.connect(region, addresses, target) - - if not ignore_pool: - entrance_pool.remove(entrancename) - if mark_two_way: - exit_pool.remove(exitname) - - if not suppress_spoiler: - world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) - - -def connect_exit(world, exitname, entrancename, player, mark_two_way=True): - if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): - logging.getLogger('').debug('Connecting %s -> %s', exitname, entrancename) - if mark_two_way: - assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename - assert exitname in exit_pool, 'Exit not in pool: ' + exitname - - entrance = world.get_entrance(entrancename, player) - exit = world.get_entrance(exitname, player) - - # if this was already connected somewhere, remove the backreference - if exit.connected_region is not None: - exit.connected_region.entrances.remove(exit) - - exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) - - if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): - if mark_two_way: - entrance_pool.remove(entrancename) - elif world.shuffle[player] == 'insanity': - entrance_exits.append(entrancename) - exit_pool.remove(exitname) - - if not suppress_spoiler: - world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) - - -def connect_two_way(world, entrancename, exitname, player): - if not ignore_pool: - logging.getLogger('').debug('Connecting %s <-> %s', entrancename, exitname) - assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename - assert exitname in exit_pool, 'Exit not in pool: ' + exitname - - entrance = world.get_entrance(entrancename, player) - exit = world.get_entrance(exitname, player) - - # if these were already connected somewhere, remove the backreference - if entrance.connected_region is not None: - entrance.connected_region.entrances.remove(entrance) - if exit.connected_region is not None: - exit.connected_region.entrances.remove(exit) - - entrance.connect(exit.parent_region, door_addresses[entrance.name][0], exit_ids[exit.name][0]) - exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) - - if not ignore_pool: - entrance_pool.remove(entrancename) - exit_pool.remove(exitname) - if world.shuffle[player] == 'insanity': - entrance_exits.append(entrancename) - - if not suppress_spoiler: - world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) - - -def connect_random(world, exitlist, targetlist, player, two_way=False): - targetlist = list(targetlist) - random.shuffle(targetlist) - - for exit, target in zip(exitlist, targetlist): - if two_way: - connect_two_way(world, exit, target, player) - else: - connect_entrance(world, exit, target, player) - - -def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must_deplete_mustexits=True): - # Keeps track of entrances that cannot be used to access each exit / cave - invalid_cave_connections = defaultdict(set) - - # if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: - # import OverworldGlitchRules - # for entrance in OverworldGlitchRules.get_non_mandatory_exits(world, player): - # if entrance in must_be_exits: - # must_be_exits.remove(entrance) - # entrances.append(entrance) - - # for insanity use only - def extract_reachable_exit(cavelist): - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - return candidate - - """This works inplace""" - random.shuffle(entrances) - random.shuffle(caves) - - used_caves = [] - required_entrances = 0 # Number of entrances reserved for used_caves - skip_remaining = False - while must_be_exits and not skip_remaining: - exit = must_be_exits.pop() - - # find multi exit cave - # * this is a mess, but it ensures a loose assignment possibility when the cave/entrance pool is plentiful, - # * but can also find and prepare for solutions when the cave/entrance pool is limiting - # * however, this probably could be better implemented - cave = None - if world.shuffle[player] == 'insanity': - cave = extract_reachable_exit(caves) - else: - if must_deplete_mustexits: - cave_surplus = sum(0 if isinstance(x, str) else len(x) - 1 for x in caves) - (len(must_be_exits) + 1) - if cave_surplus < 0: - raise RuntimeError('Not enough multi-entrance caves left to connect unreachable regions!') - if len(entrances) < len(must_be_exits) + 1: - raise RuntimeError('Not enough entrances left to connect unreachable regions!') - if cave_surplus > len(must_be_exits): - for candidate in caves: - if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): - cave = candidate - break - if len(must_be_exits) == 0: # if assigning last must exit - for candidate in caves: - if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): - cave = candidate - break - if cave is None and cave_surplus <= 1: # if options are limited - # attempt to find use caves already used - for candidate in caves: - if not isinstance(candidate, str) and candidate in used_caves: - cave = candidate - break - if cave is None: - # attempt to find caves with exact number of exits - for candidate in caves: - if not isinstance(candidate, str) and (len(entrances) - required_entrances - 1) - len(candidate) == 0: - cave = candidate - break - if cave is None: - # attempt to find caves with one left over exit - for candidate in caves: - if not isinstance(candidate, str) and (len(entrances) - required_entrances - 1) - len(candidate) == 1: - cave = candidate - break - - if cave is None: - for candidate in caves: - if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): - cave = candidate - break - if cave is None and must_deplete_mustexits: - for candidate in caves: - if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): - cave = candidate - break - - inaccessible_entrances = list() - for region_name in world.inaccessible_regions[player]: - region = world.get_region(region_name, player) - if region.type in [RegionType.LightWorld, RegionType.DarkWorld]: - for x in region.exits: - if not x.connected_region and x.name in entrance_pool: - inaccessible_entrances.append(x.name) - - if cave is None: - if must_deplete_mustexits: - raise RuntimeError('No more caves left. Should not happen!') - else: - must_be_exits.append(exit) - skip_remaining = True - continue - - # all caves are sorted so that the last exit is always reachable - if world.shuffle[player] == 'insanity': - connect_exit(world, cave[-1], exit, player, False) - entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) - entrances.remove(entrance) - connect_entrance(world, entrance, cave[-1], player, False) - else: - connect_two_way(world, exit, cave[-1], player) - - if len(cave) == 2: - entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances and e not in invalid_cave_connections[tuple(cave)]) - entrances.remove(entrance) - if world.shuffle[player] == 'insanity': - connect_entrance(world, entrance, cave[0], player, False) - entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) - entrances.remove(entrance) - connect_exit(world, cave[0], entrance, player, False) - else: - connect_two_way(world, entrance, cave[0], player) - if cave in used_caves: - required_entrances -= 2 - used_caves.remove(cave) - elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit - cave_entrances = [] - for cave_exit in cave[:-1]: - entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances) - cave_entrances.append(entrance) - entrances.remove(entrance) - if world.shuffle[player] == 'insanity': - connect_entrance(world, entrance, cave_exit, player, False) - entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances) - cave_entrances.append(entrance) - entrances.remove(entrance) - connect_exit(world, cave_exit, entrance, player, False) - else: - connect_two_way(world, entrance, cave_exit, player) - else: # save for later so we can connect to multiple exits - if cave in used_caves: - required_entrances -= 1 - used_caves.remove(cave) - else: - required_entrances += len(cave)-1 - caves.append(cave[0:-1]) - random.shuffle(caves) - used_caves.append(cave[0:-1]) - invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(inaccessible_entrances).union(entrance_exits) - caves.remove(cave) - - find_inaccessible_regions(world, player) - - for cave in used_caves: - if cave in caves: # check if we placed multiple entrances from this 3 or 4 exit - for cave_exit in cave: - entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances and e not in invalid_cave_connections[tuple(cave)]) - invalid_cave_connections[tuple(cave)] = set() - entrances.remove(entrance) - if world.shuffle[player] == 'insanity': - connect_entrance(world, entrance, cave_exit, player, False) - entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) - entrances.remove(entrance) - connect_exit(world, cave_exit, entrance, player, False) - else: - connect_two_way(world, entrance, cave_exit, player) - caves.remove(cave) - - -def connect_caves(world, lw_entrances, dw_entrances, caves, player): - """This works inplace""" - random.shuffle(lw_entrances) - random.shuffle(dw_entrances) - random.shuffle(caves) - while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill - cave_candidate = (None, 0) - for i, cave in enumerate(caves): - if isinstance(cave, str): - cave = (cave,) - if len(cave) > cave_candidate[1]: - cave_candidate = (i, len(cave)) - cave = caves.pop(cave_candidate[0]) - - target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances - if isinstance(cave, str): - cave = (cave,) - - # check if we can still fit the cave into our target group - if len(target) < len(cave): - # need to use other set - target = lw_entrances if target is dw_entrances else dw_entrances - - for exit in cave: - connect_two_way(world, target.pop(), exit, player) - - -def connect_doors(world, doors, targets, player): - """This works inplace""" - random.shuffle(doors) - random.shuffle(targets) - placing = min(len(doors), len(targets)) - for door, target in zip(doors, targets): - connect_entrance(world, door, target, player) - doors[:] = doors[placing:] - targets[:] = targets[placing:] - - -def scramble_holes(world, player): - invFlag = world.mode[player] == 'inverted' - - hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), - ('Bat Cave Cave', 'Bat Cave Drop'), - ('North Fairy Cave', 'North Fairy Cave Drop'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), - ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), - ('Sanctuary', 'Sanctuary Grave')] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - - # force uncle cave - if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - else: - hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - - if world.shuffle_ganon[player]: - hole_entrances.append(('Pyramid Entrance', 'Pyramid Hole') if not world.is_tile_swapped(0x1b, player) else ('Inverted Pyramid Entrance', 'Inverted Pyramid Hole')) - hole_targets.append(('Pyramid Exit', 'Pyramid')) - - # shuffle sanctuary hole in same world as other HC entrances - if world.shuffle[player] not in ['lean', 'crossed']: - drop_owid_map = { # owid, is_light_world - 'Lost Woods Hideout Stump': (0x00, True), - 'Lumberjack Tree Cave': (0x02, True), - 'Sanctuary': (0x13, True), - 'North Fairy Cave': (0x15, True), - 'Kakariko Well Cave': (0x18, True), - 'Hyrule Castle Secret Entrance Stairs': (0x1b, True), - 'Bat Cave Cave': (0x22, True), - 'Inverted Pyramid Entrance': (0x1b, True), - 'Pyramid Entrance': (0x5b, False) - } - - region = world.get_entrance('Hyrule Castle Exit (South)', player).parent_region - if len(region.entrances) > 0: - hc_in_lw = region.entrances[0].parent_region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld) - elif world.shuffle[player] == 'lite': - hc_in_lw = not invFlag - else: - # checks if drop candidates exist in LW - drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] - hc_in_lw = any([not world.is_tile_swapped(owid, player) for owid in drop_owids]) - - candidate_drops = list() - for door, drop in hole_entrances: - if hc_in_lw == (drop_owid_map[door][1] == (not world.is_tile_swapped(drop_owid_map[door][0], player))): - candidate_drops.append(tuple((door, drop))) - - random.shuffle(candidate_drops) - door, drop = candidate_drops.pop() - hole_entrances.remove((door, drop)) - connect_two_way(world, door, 'Sanctuary Exit', player) - connect_entrance(world, drop, 'Sewer Drop', player) - else: - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - # place pyramid hole - if not world.shuffle_ganon[player]: - exit, target = ('Pyramid Exit', 'Pyramid') - if not world.is_tile_swapped(0x1b, player): - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) - - # shuffle the rest - random.shuffle(hole_entrances) - random.shuffle(hole_targets) - for entrance, drop in hole_entrances: - exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) - - -def skull_woods_shuffle(world, player): - connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], - ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle', 'Skull Back Drop'], player) - connect_random(world, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'], - ['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'], player, True) - - -def simple_shuffle_dungeons(world, player): - invFlag = world.mode[player] == 'inverted' - - skull_woods_shuffle(world, player) - - dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] - dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Agahnims Tower Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] - - if not invFlag: - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - else: - dungeon_entrances.append('Ganons Tower') - dungeon_exits.append('Ganons Tower Exit') - random.shuffle(dungeon_exits) - at_door = dungeon_exits.pop() - else: - dungeon_entrances.append('Ganons Tower') - if not world.shuffle_ganon[player]: - at_door = 'Ganons Tower Exit' - else: - dungeon_exits.append('Ganons Tower Exit') - random.shuffle(dungeon_exits) - at_door = dungeon_exits.pop() - - # shuffle single-entrance dungeons - connect_random(world, dungeon_entrances, dungeon_exits, player, True) - - # shuffle multi-entrance dungeons - multi_dungeons = ['Desert Palace', 'Turtle Rock'] - if world.mode[player] == 'standard' or (world.mode[player] == 'inverted' and not world.shuffle_ganon): - hc_target = 'Hyrule Castle' - random.shuffle(multi_dungeons) - else: - multi_dungeons.append('Hyrule Castle') - - dungeon_owid_map = { # owid, is_lw_dungeon - 'Hyrule Castle': (0x1b, True), - 'Desert Palace': (0x30, True), - 'Turtle Rock': (0x47, False) - } - - # checks if drop candidates exist in LW - drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] - drops_in_light_world = any([not world.is_tile_swapped(owid, player) for owid in drop_owids]) - - # placing HC in guaranteed same-world as available dropdowns - if not drops_in_light_world or not invFlag: - candidate_dungeons = list() - for d in multi_dungeons: - if not drops_in_light_world and dungeon_owid_map[d][1] == world.is_tile_swapped(dungeon_owid_map[d][0], player): - # only adding DW candidates - candidate_dungeons.append(d) - elif not invFlag and dungeon_owid_map[d][1] == (not world.is_tile_swapped(dungeon_owid_map[d][0], player)): - # only adding LW candidates - candidate_dungeons.append(d) - random.shuffle(candidate_dungeons) - hc_target = candidate_dungeons.pop() - multi_dungeons.remove(hc_target) - random.shuffle(multi_dungeons) - else: - random.shuffle(multi_dungeons) - hc_target = multi_dungeons.pop() - - dp_target = multi_dungeons.pop() - tr_target = multi_dungeons.pop() - - if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', at_door, player) - elif hc_target == 'Desert Palace': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', at_door, player) - elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - if not world.is_tile_swapped(0x45, player): - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', at_door, player) - else: - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', at_door, player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) - - if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) - elif dp_target == 'Desert Palace': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) - elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) - - if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Desert Palace': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) - - -def full_shuffle_dungeons(world, Dungeon_Exits, player): - invFlag = world.mode[player] == 'inverted' - - skull_woods_shuffle(world, player) - - dungeon_exits = list(Dungeon_Exits) - - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - - if not world.shuffle_ganon[player]: - connect_two_way(world, 'Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', 'Ganons Tower Exit', player) - else: - dungeon_exits.append('Ganons Tower Exit') - - # determine LW and DW entrances - # owid: (entrances, is_light_world) - dungeon_owid_map = {0x03: ({'Tower of Hera'}, True), - 0x1e: ({'Eastern Palace'}, True), - 0x1b: ({'Hyrule Castle Entrance (South)', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower'}, True), - 0x30: ({'Desert Palace Entrance (South)', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (East)', - 'Desert Palace Entrance (North)'}, True), - 0x40: ({'Skull Woods Final Section'}, False), - 0x43: ({'Ganons Tower'}, False), - 0x45: ({'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Turtle Rock Isolated Ledge Entrance'}, False), - 0x47: ({'Turtle Rock'}, False), - 0x58: ({'Thieves Town'}, False), - 0x5e: ({'Palace of Darkness'}, False), - 0x70: ({'Misery Mire'}, False), - 0x75: ({'Ice Palace'}, False), - 0x7b: ({'Swamp Palace'}, False) - } - - lw_entrances = list() - dw_entrances = list() - for owid in dungeon_owid_map.keys(): - if dungeon_owid_map[owid][1] == (not world.is_tile_swapped(owid, player)): - lw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) - else: - dw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) - - # determine must-exit entrances - find_inaccessible_regions(world, player) - - lw_must_exit = list() - dw_must_exit = list() - lw_related = list() - dw_related = list() - if not world.is_tile_swapped(0x45, player): - dw_entrances.remove('Turtle Rock Isolated Ledge Entrance') - dw_must_exit.append('Turtle Rock Isolated Ledge Entrance') - if 'Dark Death Mountain Ledge' in world.inaccessible_regions[player]: - ledge = ['Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'] - dw_entrances = [e for e in dw_entrances if e not in ledge] - random.shuffle(ledge) - dw_must_exit.append(ledge.pop()) - dw_related.extend(ledge) - if not world.is_tile_swapped(0x30, player): - if 'Desert Mouth' in world.inaccessible_regions[player]: - lw_entrances.remove('Desert Palace Entrance (East)') - lw_must_exit.append('Desert Palace Entrance (East)') - else: - dw_entrances.remove('Desert Palace Entrance (East)') - dw_must_exit.append('Desert Palace Entrance (East)') - if 'Desert Ledge' in world.inaccessible_regions[player]: - ledge = ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] - dw_entrances = [e for e in dw_entrances if e not in ledge] - random.shuffle(ledge) - dw_must_exit.append(ledge.pop()) - dw_related.extend(ledge) - if not world.is_tile_swapped(0x1b, player): - if 'Hyrule Castle Ledge' in world.inaccessible_regions[player]: - ledge = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] - lw_entrances = [e for e in lw_entrances if e not in ledge] - random.shuffle(ledge) - lw_must_exit.append(ledge.pop()) - lw_related.extend(ledge) - random.shuffle(lw_must_exit) - random.shuffle(dw_must_exit) - - # place HC first, needs to be same world as Sanc drop - hyrule_castle_exits = ('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)') - hyrule_castle_exits = list([tuple(e for e in hyrule_castle_exits if e in exit_pool)]) - hyrule_castle_exits.extend([e for e in dungeon_exits if isinstance(e, str)]) - dungeon_exits = [e for e in dungeon_exits if not isinstance(e, str)] - if not world.is_tile_swapped(0x13, player): - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) - dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) - hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) - dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) - hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] - connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) - - # connect any remaining must-exit entrances - dungeon_exits.extend(hyrule_castle_exits) - connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_must_exit, player) - connect_mandatory_exits(world, dw_entrances, dungeon_exits, dw_must_exit, player) - - # shuffle the remaining entrances - lw_entrances = lw_entrances + lw_related - dw_entrances = dw_entrances + dw_related - connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - - -def place_links_house(world, player, ignore_list=[]): - invFlag = world.mode[player] == 'inverted' - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop' - else: - if invFlag: - for dark_sanc in world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.exits: - if dark_sanc.connected_region and dark_sanc.connected_region.name == 'Dark Sanctuary Hint': - dark_sanc = dark_sanc.name - break - - if invFlag and isinstance(dark_sanc, str): - links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, player) if i in entrance_pool] - else: - links_house_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] - if world.is_bombshop_start(player) and world.is_tile_swapped(0x03, player): - links_house_doors = [x for x in links_house_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] - if world.shuffle[player] in ['lite', 'lean']: - links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + - (default_shop_connections if world.shopsanity[player] else []) + - (default_pot_connections if world.pottery[player] not in ['none', 'keys', 'dungeon'] else []) + - (default_takeany_connections if world.take_any[player] == 'fixed' else []))))[0]] - - #TODO: Need to improve Links House placement to choose a better sector or eliminate entrances that are after ledge drops - links_house_doors = [e for e in links_house_doors if e not in ignore_list] - assert len(links_house_doors), 'No valid candidates to place Links House' - links_house = random.choice(links_house_doors) - if not world.is_bombshop_start(player): - connect_two_way(world, links_house, 'Links House Exit', player) - else: - connect_entrance(world, links_house, 'Big Bomb Shop', player) - world.get_entrance('Big Bomb Shop Exit', player).connect(world.get_entrance(links_house, player).parent_region) - return links_house - - -def place_dark_sanc(world, player, ignore_list=[]): - if not world.shufflelinks[player]: - sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop' if world.is_bombshop_start(player) else 'Links House', player) if i in entrance_pool] - else: - sanc_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] - if world.shuffle[player] in ['lite', 'lean']: - sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] - - sanc_doors = [e for e in sanc_doors if e not in ignore_list] - assert len(sanc_doors), 'No valid candidates to place Dark Chapel' - sanc_door = random.choice(sanc_doors) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - return sanc_door - - -def place_blacksmith(world, links_house, player): - invFlag = world.mode[player] == 'inverted' - - assumed_inventory = list() - if world.logic[player] in ['noglitches', 'minorglitches'] and (world.is_tile_swapped(0x29, player) == invFlag): - assumed_inventory.append('Titans Mitts') - - links_region = world.get_entrance(links_house, player).parent_region.name - blacksmith_doors = list(build_accessible_entrance_list(world, links_region, player, assumed_inventory, False, True, True)) - - if invFlag: - dark_sanc = world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.name - blacksmith_doors = list(OrderedDict.fromkeys(blacksmith_doors + list(build_accessible_entrance_list(world, dark_sanc, player, assumed_inventory, False, True, True)))) - elif world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: - sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name - blacksmith_doors = list(OrderedDict.fromkeys(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) - if world.shuffle[player] in ['lite', 'lean']: - blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + - (default_shop_connections if world.shopsanity[player] else []) + - (default_pot_connections if world.pottery[player] not in ['none', 'keys', 'dungeon'] else []) + - (default_takeany_connections if world.take_any[player] == 'fixed' else []))))[0]] - - assert len(blacksmith_doors), 'No valid candidates to place Blacksmiths Hut' - blacksmith_hut = random.choice(blacksmith_doors) - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - return blacksmith_hut - - -def place_old_man(world, pool, player, ignore_list=[]): - # exit has to come from specific set of doors, the entrance is free to move about - if not world.is_tile_swapped(0x03, player): - region_name = 'West Death Mountain (Top)' - else: - region_name = 'West Dark Death Mountain (Top)' - old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True, True)) - old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)' and e not in ignore_list] - if world.shuffle[player] in ['lite', 'lean']: - old_man_entrances = [e for e in old_man_entrances if e in pool] - assert len(old_man_entrances), 'No available entrances left to place Old Man Cave' - random.shuffle(old_man_entrances) - old_man_exit = None - while not old_man_exit: - old_man_exit = old_man_entrances.pop() - if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True, True): - old_man_exit = None - - old_man_entrances = [e for e in pool if e in entrance_pool and e not in ignore_list and e not in entrance_exits + [old_man_exit]] - random.shuffle(old_man_entrances) - old_man_entrance = old_man_entrances.pop() - if world.shuffle[player] != 'insanity': - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - else: - # skip assigning connections to West Entrance/Exit - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player, False) - connect_entrance(world, old_man_entrance, 'Old Man Cave Exit (East)', player, False) - - -def junk_fill_inaccessible(world, player): - from Main import copy_world_premature - find_inaccessible_regions(world, player) - - for p in range(1, world.players + 1): - world.key_logic[p] = {} - base_world = copy_world_premature(world, player) - base_world.override_bomb_check = True - - # remove regions that have a dungeon entrance - accessible_regions = list() - for region_name in world.inaccessible_regions[player]: - region = world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: - accessible_regions.append(region_name) - break - for region_name in accessible_regions.copy(): - accessible_regions = list(OrderedDict.fromkeys(accessible_regions + list(build_accessible_region_list(base_world, region_name, player, False, True, False, False)))) - world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] - - # get inaccessible entrances - inaccessible_entrances = list() - for region_name in world.inaccessible_regions[player]: - region = world.get_region(region_name, player) - if region.type in [RegionType.LightWorld, RegionType.DarkWorld]: - for exit in region.exits: - if not exit.connected_region and exit.name in entrance_pool: - inaccessible_entrances.append(exit.name) - - junk_locations = [e for e in list(zip(*(default_connections + - ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections) + - ([] if world.take_any[player] == 'fixed' else default_takeany_connections))))[1] if e in exit_pool] - random.shuffle(junk_locations) - for entrance in inaccessible_entrances: - connect_entrance(world, entrance, junk_locations.pop(), player) - - -def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list=[]): - def find_inacessible_ow_regions(): - nonlocal inaccessible_regions - find_inaccessible_regions(world, player) - inaccessible_regions = list(world.inaccessible_regions[player]) - - # find OW regions that don't have a multi-entrance dungeon exit connected - glitch_regions = ['Central Cliffs', 'Eastern Cliff', 'Desert Northern Cliffs', 'Hyrule Castle Water', - 'Dark Central Cliffs', 'Darkness Cliff', 'Mire Northern Cliffs', 'Pyramid Water'] - multi_dungeon_exits = { - 'Hyrule Castle South Portal', 'Hyrule Castle West Portal', 'Hyrule Castle East Portal', 'Sanctuary Portal', - 'Desert South Portal', 'Desert West Portal', - 'Skull 2 East Portal', 'Skull 2 West Portal', - 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Eye Bridge Portal' - } - for region_name in world.inaccessible_regions[player]: - if (world.logic[player] in ['noglitches', 'minorglitches'] and region_name in glitch_regions) \ - or (region_name == 'Pyramid Exit Ledge' and (world.shuffle[player] not in ['district', 'insanity'] or world.is_tile_swapped(0x1b, player))) \ - or (region_name == 'Spiral Mimic Ledge Extend' and not world.is_tile_swapped(0x05, player)): - # removing irrelevant and resolved regions - inaccessible_regions.remove(region_name) - continue - region = world.get_region(region_name, player) - if region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: - inaccessible_regions.remove(region_name) - continue - if world.shuffle[player] != 'insanity': - for exit in region.exits: - # because dungeon regions haven't been connected yet, the inaccessibility check won't be able to know it's reachable yet - if exit.connected_region and exit.connected_region.name in multi_dungeon_exits: - resolved_regions.append(region_name) - break - - inaccessible_regions = list() - resolved_regions = list() - find_inacessible_ow_regions() - - # keep track of neighboring regions for later consolidation - must_exit_links = OrderedDict() - for region_name in inaccessible_regions: - region = world.get_region(region_name, player) - must_exit_links[region_name] = [x.connected_region.name for x in region.exits if x.connected_region and x.connected_region.name in inaccessible_regions] - - # group neighboring regions together, separated by one-ways - def consolidate_group(region): - processed_regions.append(region) - must_exit_links_copy.pop(region) - region_group.append(region) - for dest_region in must_exit_links[region]: - if region in must_exit_links[dest_region]: - if dest_region not in processed_regions: - consolidate_group(dest_region) - else: - one_ways.append(tuple((region, dest_region))) - - processed_regions = list() - must_exit_candidates = list() - one_ways = list() - must_exit_links_copy = must_exit_links.copy() - while len(must_exit_links_copy): - region_group = list() - region_name = next(iter(must_exit_links_copy)) - consolidate_group(region_name) - must_exit_candidates.append(region_group) - - # get available entrances in each group - for regions in must_exit_candidates: - entrances = list() - for region_name in regions: - region = world.get_region(region_name, player) - entrances = entrances + [x.name for x in region.exits if x.spot_type == 'Entrance' and not x.connected_region] - entrances = [e for e in entrances if e in entrance_pool and e not in ignore_list] - must_exit_candidates[must_exit_candidates.index(regions)] = tuple((regions, entrances)) - - # necessary for circular relations between region groups, it will pick the last group - # and fill one of those entrances, and we don't want it to bias the same group - random.shuffle(must_exit_candidates) - - # remove must exit candidates that would be made accessible thru other region groups - def find_group(region): - for group in must_exit_candidates: - regions, _ = group - if region in regions: - return group - raise Exception(f'Could not find region group for {region}') - - def cascade_ignore(group): - nonlocal ignored_regions - regions, _ = group - ignored_regions = ignored_regions + regions - for from_region, to_region in one_ways: - if from_region in regions and to_region not in ignored_regions: - cascade_ignore(find_group(to_region)) - - def process_group(group): - nonlocal processed_regions, ignored_regions - regions, entrances = group - must_exit_candidates_copy.remove(group) - processed_regions = processed_regions + regions - if regions[0] not in ignored_regions: - for from_region, to_region in one_ways: - if to_region in regions and from_region not in ignored_regions + processed_regions: - process_group(find_group(from_region)) # process the parent region group - if regions[0] not in ignored_regions: - # this is the top level region - if any(r in resolved_regions for r in regions): - cascade_ignore(group) - else: - if len(entrances): - # we will fulfill must exit here and cascade access to children - must_exit_regions.append(group) - cascade_ignore(group) - else: - ignored_regions = ignored_regions + regions - - processed_regions = list() - ignored_regions = list() - must_exit_regions = list() - must_exit_candidates_copy = must_exit_candidates.copy() - while len(must_exit_candidates_copy): - region_group = next(iter(must_exit_candidates_copy)) - process_group(region_group) - - # connect must exits - random.shuffle(must_exit_regions) - must_exits_lw = list() - must_exits_dw = list() - for regions, entrances in must_exit_regions: - region = world.get_region(regions[0], player) - if region.type == RegionType.LightWorld: - must_exits_lw.append(random.choice(entrances)) - else: - must_exits_dw.append(random.choice(entrances)) - if world.shuffle[player] in ['lean', 'crossed', 'insanity']: # cross world - pool = [e for e in lw_entrances + dw_entrances if e in entrance_pool and e not in must_exits_lw + must_exits_dw] - connect_mandatory_exits(world, pool, caves, must_exits_lw + must_exits_dw, player) - else: - pool = [e for e in lw_entrances if e in entrance_pool and e not in must_exits_lw] - if len(pool): - connect_mandatory_exits(world, pool, caves, must_exits_lw, player) - pool = [e for e in dw_entrances if e in entrance_pool and e not in must_exits_dw] - if len(pool): - connect_mandatory_exits(world, pool, caves, must_exits_dw, player) - - # check accessibility afterwards - resolved_regions = list() - find_inacessible_ow_regions() - inaccessible_regions = [e for e in inaccessible_regions if e not in resolved_regions] - # TODO: Instead of line above, this should cascade from the resolved regions down to regions it can access - if len(inaccessible_regions) > 0: - logging.getLogger('').debug(f'Could not resolve inaccessible regions: [{", ".join(inaccessible_regions)}]') - logging.getLogger('').debug(f'^ This is most often a false positive because Dungeon regions aren\'t connected yet') - - -def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): - def shuffle_lists_in_list(ls): - for i, item in enumerate(ls): - if isinstance(item, list): - ls[i] = random.sample(item, len(item)) - - def tuplize_lists_in_list(ls): - for i, item in enumerate(ls): - if isinstance(item, list): - ls[i] = tuple(item) - - shuffle_lists_in_list(Dungeon_Exits) - shuffle_lists_in_list(Cave_Exits) - shuffle_lists_in_list(Old_Man_House) - shuffle_lists_in_list(Cave_Three_Exits) - - # paradox fixup - if Cave_Three_Exits[1][0] == "Paradox Cave Exit (Bottom)": - i = random.randint(1,2) - Cave_Three_Exits[1][0] = Cave_Three_Exits[1][i] - Cave_Three_Exits[1][i] = "Paradox Cave Exit (Bottom)" - - # TR fixup - tr_fixup = False - for i, item in enumerate(Dungeon_Exits[-1]): - if 'Turtle Rock Ledge Exit (East)' == item: - tr_fixup = True - if 0 != i: - Dungeon_Exits[-1][i] = Dungeon_Exits[-1][0] - Dungeon_Exits[-1][0] = 'Turtle Rock Ledge Exit (East)' - break - - if not tr_fixup: raise RuntimeError("TR entrance shuffle fixup didn't happen") - - tuplize_lists_in_list(Dungeon_Exits) - tuplize_lists_in_list(Cave_Exits) - tuplize_lists_in_list(Old_Man_House) - tuplize_lists_in_list(Cave_Three_Exits) - - -def unbias_dungeons(Dungeon_Exits): - def shuffle_lists_in_list(ls): - for i, item in enumerate(ls): - if isinstance(item, list): - ls[i] = random.sample(item, len(item)) - - def tuplize_lists_in_list(ls): - for i, item in enumerate(ls): - if isinstance(item, list): - ls[i] = tuple(item) - - shuffle_lists_in_list(Dungeon_Exits) - - # TR fixup - for i, item in enumerate(Dungeon_Exits[-1]): - if 'Turtle Rock Ledge Exit (East)' == item: - if 0 != i: - Dungeon_Exits[-1][i] = Dungeon_Exits[-1][0] - Dungeon_Exits[-1][0] = 'Turtle Rock Ledge Exit (East)' - break - - tuplize_lists_in_list(Dungeon_Exits) - - -def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): - from Main import copy_world_premature - from Items import ItemFactory - from OverworldShuffle import one_way_ledges - - for p in range(1, world.players + 1): - world.key_logic[p] = {} - base_world = copy_world_premature(world, player) - base_world.override_bomb_check = True - - connect_simple(base_world, 'Links House S&Q', start_region, player) - blank_state = CollectionState(base_world) - if base_world.mode[player] == 'standard': - blank_state.collect(ItemFactory('Zelda Delivered', player), True) - for item in assumed_inventory: - blank_state.collect(ItemFactory(item, player), True) - - explored_regions = list(build_accessible_region_list(base_world, start_region, player, False, cross_world, region_rules, False)) - - if include_one_ways: - new_regions = list() - for region_name in explored_regions: - if region_name in one_way_ledges: - for ledge in one_way_ledges[region_name]: - if ledge not in explored_regions + new_regions: - new_regions.append(ledge) - explored_regions.extend(new_regions) - - entrances = list() - for region_name in explored_regions: - region = base_world.get_region(region_name, player) - for exit in region.exits: - if exit.name in entrance_pool and (not exit_rules or exit.access_rule(blank_state)): - entrances.append(exit.name) - - return entrances - - -def get_starting_entrances(world, player, force_starting_world=True): - invFlag = world.mode[player] == 'inverted' - - # find largest walkable sector - sector = None - invalid_sectors = list() - entrances = list() - while not len(entrances): - while (sector is None): - sector = max(world.owsectors[player], key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) - if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ - and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): - invalid_sectors.append(sector) - sector = None - regions = max(sector, key=lambda x: len(x)) - - # get entrances from list of regions - entrances = list() - for region_name in regions: - if world.shuffle[player] == 'simple' and region_name in OWTileRegions.keys() and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: - continue - region = world.get_region(region_name, player) - if not force_starting_world or region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): - for exit in region.exits: - if not exit.connected_region and exit.spot_type == 'Entrance': - entrances.append(exit.name) - - invalid_sectors.append(sector) - sector = None - - return entrances - - -def get_distant_entrances(world, start_entrance, player): - from OverworldShuffle import one_way_ledges - - # get walkable sector in which initial entrance was placed - start_region = world.get_entrance(start_entrance, player).parent_region.name - regions = next(s for s in world.owsectors[player] if any(start_region in w for w in s)) - regions = next(w for w in regions if start_region in w) - - # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain - explored_regions = list({start_region}) - was_progress = True - while was_progress and len(explored_regions) < len(regions) / 2: - was_progress = False - new_regions = list() - for region_name in explored_regions: - if region_name in one_way_ledges: - for ledge in one_way_ledges[region_name]: - if ledge not in explored_regions + new_regions: - new_regions.append(ledge) - was_progress = True - region = world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region and region.type == exit.connected_region.type and exit.connected_region.name in regions and exit.connected_region.name not in explored_regions + new_regions: - new_regions.append(exit.connected_region.name) - was_progress = True - explored_regions.extend(new_regions) - - # get entrances from remaining regions - candidates = list() - for region_name in [r for r in regions if r not in explored_regions]: - if region_name in OWTileRegions.keys() and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: - continue - region = world.get_region(region_name, player) - for exit in region.exits: - if not exit.connected_region and exit.spot_type == 'Entrance': - candidates.append(exit.name) - - return candidates - - -def can_reach(world, entrance_name, region_name, player): - from Main import copy_world_premature - from Items import ItemFactory - - for p in range(1, world.players + 1): - world.key_logic[p] = {} - base_world = copy_world_premature(world, player) - base_world.override_bomb_check = True - - entrance = world.get_entrance(entrance_name, player) - connect_simple(base_world, 'Links House S&Q', entrance.parent_region.name, player) - blank_state = CollectionState(base_world) - if base_world.mode[player] == 'standard': - blank_state.collect(ItemFactory('Zelda Delivered', player), True) - - find_inaccessible_regions(world, player) - return region_name not in world.inaccessible_regions[player] - - -LW_Dungeon_Exits = [('Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'), - 'Desert Palace Exit (North)', - 'Eastern Palace Exit', - 'Tower of Hera Exit', - 'Agahnims Tower Exit'] - -DW_Late_Dungeon_Exits = ['Ice Palace Exit', - 'Misery Mire Exit', - ('Turtle Rock Ledge Exit (East)', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit')] - -DW_Mid_Dungeon_Exits = ['Thieves Town Exit', - 'Skull Woods Final Section Exit', - 'Palace of Darkness Exit', - 'Swamp Palace Exit'] - -Cave_Exits_Base = [('Elder House Exit (East)', 'Elder House Exit (West)'), - ('Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'), - ('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'), - ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'), - ('Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'), - ('Hookshot Cave Back Exit', 'Hookshot Cave Front Exit')] - -Cave_Exits_Directional = [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'), - ('Spiral Cave Exit (Top)', 'Spiral Cave Exit')] - -Cave_Three_Exits_Base = [('Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave Exit'), - ('Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Bottom)')] - -Old_Man_House_Base = [('Old Man House Exit (Bottom)', 'Old Man House Exit (Top)')] - - -Entrance_Pool_Base = ['Links House', - 'Desert Palace Entrance (South)', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (East)', - 'Desert Palace Entrance (North)', - 'Eastern Palace', - 'Tower of Hera', - 'Hyrule Castle Entrance (South)', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower', - 'Thieves Town', - 'Skull Woods First Section Door', - 'Skull Woods Second Section Door (East)', - 'Skull Woods Second Section Door (West)', - 'Skull Woods Final Section', - 'Ice Palace', - 'Misery Mire', - 'Palace of Darkness', - 'Swamp Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Turtle Rock Isolated Ledge Entrance', - 'Hyrule Castle Secret Entrance Stairs', - 'Kakariko Well Cave', - 'Bat Cave Cave', - 'Elder House (East)', - 'Elder House (West)', - 'North Fairy Cave', - 'Lost Woods Hideout Stump', - 'Lumberjack Tree Cave', - 'Two Brothers House (East)', - 'Two Brothers House (West)', - 'Sanctuary', - 'Old Man Cave (East)', - 'Old Man Cave (West)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (West)', - 'Death Mountain Return Cave (East)', - 'Spectacle Rock Cave (Bottom)', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave (Bottom)', - 'Spiral Cave', - 'Bumper Cave (Top)', - 'Bumper Cave (Bottom)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Hookshot Cave Back Entrance', - 'Ganons Tower', - 'Pyramid Entrance', - 'Waterfall of Wishing', - 'Dam', - 'Blinds Hideout', - 'Lumberjack House', - 'Bonk Fairy (Light)', - 'Bonk Fairy (Dark)', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Mire Fairy', - 'Dark Death Mountain Fairy', - 'Fortune Teller (Light)', - 'Lake Hylia Fortune Teller', - 'Kings Grave', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Lake Hylia Shop', - 'Dark Death Mountain Shop', - 'Capacity Upgrade', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Cave 45', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Bonk Rock Cave', - 'Library', - 'Kakariko Gamble Game', - 'Potion Shop', - 'Hookshot Fairy', - 'Pyramid Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Big Bomb Shop', - 'Dark World Shop', - 'Dark Lake Hylia Shop', - 'Dark Lumberjack Shop', - 'Dark Potion Shop', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Archery Game', - 'Mire Shed', - 'Mire Hint', - 'Spike Cave', - 'Mimic Cave', - 'Kakariko Well Drop', - 'Hyrule Castle Secret Entrance Drop', - 'Bat Cave Drop', - 'North Fairy Cave Drop', - 'Lost Woods Hideout Drop', - 'Lumberjack Tree Tree', - 'Sanctuary Grave', - 'Skull Woods Second Section Hole', - 'Skull Woods First Section Hole (West)', - 'Skull Woods First Section Hole (East)', - 'Skull Woods First Section Hole (North)', - 'Pyramid Hole'] - -Exit_Pool_Base = ['Links House Exit', - 'Desert Palace Exit (South)', - 'Desert Palace Exit (West)', - 'Desert Palace Exit (East)', - 'Desert Palace Exit (North)', - 'Eastern Palace Exit', - 'Tower of Hera Exit', - 'Hyrule Castle Exit (South)', - 'Hyrule Castle Exit (West)', - 'Hyrule Castle Exit (East)', - 'Agahnims Tower Exit', - 'Thieves Town Exit', - 'Skull Woods First Section Exit', - 'Skull Woods Second Section Exit (East)', - 'Skull Woods Second Section Exit (West)', - 'Skull Woods Final Section Exit', - 'Ice Palace Exit', - 'Misery Mire Exit', - 'Palace of Darkness Exit', - 'Swamp Palace Exit', - 'Turtle Rock Exit (Front)', - 'Turtle Rock Ledge Exit (West)', - 'Turtle Rock Ledge Exit (East)', - 'Turtle Rock Isolated Ledge Exit', - 'Hyrule Castle Secret Entrance Exit', - 'Kakariko Well Exit', - 'Bat Cave Exit', - 'Elder House Exit (East)', - 'Elder House Exit (West)', - 'North Fairy Cave Exit', - 'Lost Woods Hideout Exit', - 'Lumberjack Tree Exit', - 'Two Brothers House Exit (East)', - 'Two Brothers House Exit (West)', - 'Sanctuary Exit', - 'Old Man Cave Exit (East)', - 'Old Man Cave Exit (West)', - 'Old Man House Exit (Bottom)', - 'Old Man House Exit (Top)', - 'Death Mountain Return Cave Exit (West)', - 'Death Mountain Return Cave Exit (East)', - 'Spectacle Rock Cave Exit', - 'Spectacle Rock Cave Exit (Top)', - 'Spectacle Rock Cave Exit (Peak)', - 'Paradox Cave Exit (Bottom)', - 'Paradox Cave Exit (Middle)', - 'Paradox Cave Exit (Top)', - 'Fairy Ascension Cave Exit (Bottom)', - 'Fairy Ascension Cave Exit (Top)', - 'Spiral Cave Exit', - 'Spiral Cave Exit (Top)', - 'Bumper Cave Exit (Top)', - 'Bumper Cave Exit (Bottom)', - 'Superbunny Cave Exit (Top)', - 'Superbunny Cave Exit (Bottom)', - 'Hookshot Cave Front Exit', - 'Hookshot Cave Back Exit', - 'Ganons Tower Exit', - 'Pyramid Exit', - 'Waterfall of Wishing', - 'Dam', - 'Blinds Hideout', - 'Lumberjack House', - 'Bonk Fairy (Light)', - 'Bonk Fairy (Dark)', - 'Lake Hylia Healer Fairy', - 'Light Hype Fairy', - 'Desert Healer Fairy', - 'Dark Lake Hylia Healer Fairy', - 'Dark Lake Hylia Ledge Healer Fairy', - 'Mire Healer Fairy', - 'Dark Death Mountain Healer Fairy', - 'Fortune Teller (Light)', - 'Lake Hylia Fortune Teller', - 'Kings Grave', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Lake Hylia Shop', - 'Dark Death Mountain Shop', - 'Capacity Upgrade', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Cave 45', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Bonk Rock Cave', - 'Library', - 'Kakariko Gamble Game', - 'Potion Shop', - 'Hookshot Fairy', - 'Pyramid Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Big Bomb Shop', - 'Village of Outcasts Shop', - 'Dark Lake Hylia Shop', - 'Dark Lumberjack Shop', - 'Dark Potion Shop', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Archery Game', - 'Mire Shed', - 'Mire Hint', - 'Spike Cave', - 'Mimic Cave', - 'Kakariko Well (top)', - 'Hyrule Castle Secret Entrance', - 'Bat Cave (right)', - 'North Fairy Cave', - 'Lost Woods Hideout (top)', - 'Lumberjack Tree (top)', - 'Sewer Drop', - 'Skull Back Drop', - 'Skull Left Drop', - 'Skull Pinball', - 'Skull Pot Circle', - 'Pyramid'] - -# these are connections that cannot be shuffled and always exist. -# They link together underworld regions -mandatory_connections = [ - ('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'), - ('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'), - ('Death Mountain Return Cave E', 'Death Mountain Return Cave (right)'), - ('Death Mountain Return Cave W', 'Death Mountain Return Cave (left)'), - ('Old Man Cave Dropdown', 'Old Man Cave (West)'), - ('Old Man Cave W', 'Old Man Cave (West)'), - ('Old Man Cave E', 'Old Man Cave (East)'), - ('Spectacle Rock Cave Drop', 'Spectacle Rock Cave Pool'), - ('Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Pool'), - ('Spectacle Rock Cave West Edge', 'Spectacle Rock Cave (Bottom)'), - ('Spectacle Rock Cave East Edge', 'Spectacle Rock Cave Pool'), - ('Old Man House Front to Back', 'Old Man House Back'), - ('Old Man House Back to Front', 'Old Man House'), - ('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'), - ('Paradox Shop', 'Paradox Shop'), - ('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'), - ('Paradox Cave Push Block', 'Paradox Cave Front'), - ('Paradox Cave Chest Area NE', 'Paradox Cave Bomb Area'), - ('Paradox Cave Bomb Jump', 'Paradox Cave'), - ('Paradox Cave Drop', 'Paradox Cave Chest Area'), - ('Fairy Ascension Cave Climb', 'Fairy Ascension Cave (Top)'), - ('Fairy Ascension Cave Pots', 'Fairy Ascension Cave (Bottom)'), - ('Fairy Ascension Cave Drop', 'Fairy Ascension Cave (Drop)'), - ('Sewer Drop', 'Sewers Rat Path'), - ('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'), - ('Kakariko Well (top to back)', 'Kakariko Well (back)'), - ('Blinds Hideout N', 'Blinds Hideout (Top)'), - ('Bat Cave Door', 'Bat Cave (left)'), - ('Good Bee Cave Front to Back', 'Good Bee Cave (back)'), - ('Good Bee Cave Back to Front', 'Good Bee Cave'), - ('Capacity Upgrade East', 'Capacity Fairy Pool'), - ('Capacity Fairy Pool West', 'Capacity Upgrade'), - ('Bonk Fairy (Dark) Pool', 'Bonk Fairy Pool'), - ('Bonk Fairy (Light) Pool', 'Bonk Fairy Pool'), - - ('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'), - ('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'), - ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), - ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), - ('Hookshot Cave Back to Fairy', 'Hookshot Cave (Fairy Pool)'), - ('Hookshot Cave Fairy to Back', 'Hookshot Cave (Back)'), - ('Hookshot Cave Bonk Path', 'Hookshot Cave (Bonk Islands)'), - ('Hookshot Cave Hook Path', 'Hookshot Cave (Hook Islands)'), - ('Superbunny Cave Climb', 'Superbunny Cave (Top)'), - ('Bumper Cave Bottom to Top', 'Bumper Cave (top)'), - ('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'), - ('Missing Smith', 'Missing Smith'), - ('Ganon Drop', 'Bottom of Pyramid') - ] - -# non-shuffled entrance links -default_connections = [('Lost Woods Gamble', 'Lost Woods Gamble'), - ('Fortune Teller (Light)', 'Fortune Teller (Light)'), - ('Bush Covered House', 'Bush Covered House'), - ('Kakariko Gamble Game', 'Kakariko Gamble Game'), - ('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), - ('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'), - ('Long Fairy Cave', 'Long Fairy Cave'), - ('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'), - ('Good Bee Cave', 'Good Bee Cave'), - - ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), - ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), - ('Archery Game', 'Archery Game'), - ('Dark Lake Hylia Fairy', 'Dark Lake Hylia Healer Fairy'), - ('East Dark World Hint', 'East Dark World Hint'), - ('Mire Fairy', 'Mire Healer Fairy'), - ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint') - ] - -default_takeany_connections = [('Light Hype Fairy', 'Light Hype Fairy'), - ('Desert Fairy', 'Desert Healer Fairy'), - ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'), - ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), - ('Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Healer Fairy') - ] - -default_pot_connections = [('Lumberjack House', 'Lumberjack House'), - ('Hookshot Fairy', 'Hookshot Fairy'), - ('Snitch Lady (East)', 'Snitch Lady (East)'), - ('Snitch Lady (West)', 'Snitch Lady (West)'), - ('Light World Bomb Hut', 'Light World Bomb Hut'), - ('Tavern (Front)', 'Tavern (Front)'), - ('20 Rupee Cave', '20 Rupee Cave'), - ('50 Rupee Cave', '50 Rupee Cave'), - ('Palace of Darkness Hint', 'Palace of Darkness Hint'), - ('Mire Hint', 'Mire Hint'), - ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave') - ] - -default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'), - ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'), - ('Spectacle Rock Cave Peak', 'Spectacle Rock Cave Exit (Peak)'), - ('Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave Exit'), - ('Spectacle Rock Cave', 'Spectacle Rock Cave Exit (Top)'), - ('Old Man Cave (East)', 'Old Man Cave Exit (East)'), - ('Old Man Cave (West)', 'Old Man Cave Exit (West)'), - ('Old Man House (Bottom)', 'Old Man House Exit (Bottom)'), - ('Old Man House (Top)', 'Old Man House Exit (Top)'), - ('Spiral Cave', 'Spiral Cave Exit (Top)'), - ('Spiral Cave (Bottom)', 'Spiral Cave Exit'), - ('Fairy Ascension Cave (Top)', 'Fairy Ascension Cave Exit (Top)'), - ('Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave Exit (Bottom)'), - ('Paradox Cave (Bottom)', 'Paradox Cave Exit (Bottom)'), - ('Paradox Cave (Middle)', 'Paradox Cave Exit (Middle)'), - ('Paradox Cave (Top)', 'Paradox Cave Exit (Top)'), - ('Elder House (West)', 'Elder House Exit (West)'), - ('Elder House (East)', 'Elder House Exit (East)'), - ('Two Brothers House (West)', 'Two Brothers House Exit (West)'), - ('Two Brothers House (East)', 'Two Brothers House Exit (East)'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave Back Exit'), - ('Hookshot Cave', 'Hookshot Cave Front Exit'), - ('Superbunny Cave (Top)', 'Superbunny Cave Exit (Top)'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave Exit (Bottom)'), - ('Bumper Cave (Bottom)', 'Bumper Cave Exit (Bottom)'), - ('Bumper Cave (Top)', 'Bumper Cave Exit (Top)') - ] - -default_item_connections = [('Mimic Cave', 'Mimic Cave'), - ('Waterfall of Wishing', 'Waterfall of Wishing'), - ('Bonk Rock Cave', 'Bonk Rock Cave'), - ('Graveyard Cave', 'Graveyard Cave'), - ('Kings Grave', 'Kings Grave'), - ('Potion Shop', 'Potion Shop'), - ('Blinds Hideout', 'Blinds Hideout'), - ('Chicken House', 'Chicken House'), - ('Sick Kids House', 'Sick Kids House'), - ('Sahasrahlas Hut', 'Sahasrahlas Hut'), - ('Blacksmiths Hut', 'Blacksmiths Hut'), - ('Library', 'Library'), - ('Links House', 'Links House Exit'), - ('Checkerboard Cave', 'Checkerboard Cave'), - ('Aginahs Cave', 'Aginahs Cave'), - ('Cave 45', 'Cave 45'), - ('Mini Moldorm Cave', 'Mini Moldorm Cave'), - ('Ice Rod Cave', 'Ice Rod Cave'), - ('Dam', 'Dam'), - ('Spike Cave', 'Spike Cave'), - ('Chest Game', 'Chest Game'), - ('C-Shaped House', 'C-Shaped House'), - ('Brewery', 'Brewery'), - ('Pyramid Fairy', 'Pyramid Fairy'), - ('Hammer Peg Cave', 'Hammer Peg Cave'), - ('Big Bomb Shop', 'Big Bomb Shop'), - ('Mire Shed', 'Mire Shed'), - ('Hype Cave', 'Hype Cave') - ] - -default_shop_connections = [('Kakariko Shop', 'Kakariko Shop'), - ('Lake Hylia Shop', 'Lake Hylia Shop'), - ('Capacity Upgrade', 'Capacity Upgrade'), - ('Dark Lumberjack Shop', 'Dark Lumberjack Shop'), - ('Dark Death Mountain Shop', 'Dark Death Mountain Shop'), - ('Dark Potion Shop', 'Dark Potion Shop'), - ('Dark World Shop', 'Village of Outcasts Shop'), - ('Red Shield Shop', 'Red Shield Shop'), - ('Dark Lake Hylia Shop', 'Dark Lake Hylia Shop') - ] - -default_drop_connections = [('Lost Woods Hideout Drop', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Tree', 'Lumberjack Tree (top)'), - ('Sanctuary Grave', 'Sewer Drop'), - ('North Fairy Cave Drop', 'North Fairy Cave'), - ('Kakariko Well Drop', 'Kakariko Well (top)'), - ('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance'), - ('Bat Cave Drop', 'Bat Cave (right)'), - #('Pyramid Hole', 'Pyramid') # this is dynamically added because of Inverted/OW Mixed - ] - -default_dropexit_connections = [('Lost Woods Hideout Stump', 'Lost Woods Hideout Exit'), - ('Lumberjack Tree Cave', 'Lumberjack Tree Exit'), - ('Sanctuary', 'Sanctuary Exit'), - ('North Fairy Cave', 'North Fairy Cave Exit'), - ('Kakariko Well Cave', 'Kakariko Well Exit'), - ('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit'), - ('Bat Cave Cave', 'Bat Cave Exit'), - #('Pyramid Entrance', 'Pyramid Exit') # this is dynamically added because of Inverted/OW Mixed - ] - -# non shuffled dungeons -default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Palace Exit (South)'), - ('Desert Palace Entrance (West)', 'Desert Palace Exit (West)'), - ('Desert Palace Entrance (North)', 'Desert Palace Exit (North)'), - ('Desert Palace Entrance (East)', 'Desert Palace Exit (East)'), - - ('Eastern Palace', 'Eastern Palace Exit'), - ('Tower of Hera', 'Tower of Hera Exit'), - - ('Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)'), - ('Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)'), - ('Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)'), - - ('Thieves Town', 'Thieves Town Exit'), - ('Skull Woods First Section Door', 'Skull Woods First Section Exit'), - ('Skull Woods Second Section Door (East)', 'Skull Woods Second Section Exit (East)'), - ('Skull Woods Second Section Door (West)', 'Skull Woods Second Section Exit (West)'), - ('Skull Woods Final Section', 'Skull Woods Final Section Exit'), - ('Ice Palace', 'Ice Palace Exit'), - ('Misery Mire', 'Misery Mire Exit'), - ('Palace of Darkness', 'Palace of Darkness Exit'), - ('Swamp Palace', 'Swamp Palace Exit'), # requires additional patch for flooding moat if moved - - ('Turtle Rock', 'Turtle Rock Exit (Front)'), - ('Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)'), - ('Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)'), - ('Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit') - ] - -default_skulldrop_connections = [('Skull Woods First Section Hole (East)', 'Skull Pinball'), - ('Skull Woods First Section Hole (West)', 'Skull Left Drop'), - ('Skull Woods First Section Hole (North)', 'Skull Pot Circle'), - ('Skull Woods Second Section Hole', 'Skull Back Drop') - ] - -open_default_dungeon_connections = [('Ganons Tower', 'Ganons Tower Exit'), - ('Agahnims Tower', 'Agahnims Tower Exit') - ] - -inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Exit'), - ('Agahnims Tower', 'Ganons Tower Exit') - ] -# format: -# Key=Name -# addr = (door_index, exitdata, ow_flag) # multiexit -# | ([addr], None) # holes -# exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) - -# ToDo somehow merge this with creation of the locations -door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000), 0x00), - 'Desert Palace Entrance (South)': (0x08, (0x0084, 0x30, 0x0314, 0x0c56, 0x00a6, 0x0ca8, 0x0128, 0x0cc3, 0x0133, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), - 'Desert Palace Entrance (West)': (0x0A, (0x0083, 0x30, 0x0280, 0x0c46, 0x0003, 0x0c98, 0x0088, 0x0cb3, 0x0090, 0x0a, 0xfd, 0x0000, 0x0000), 0x00), - 'Desert Palace Entrance (North)': (0x0B, (0x0063, 0x30, 0x0016, 0x0c00, 0x00a2, 0x0c28, 0x0128, 0x0c6d, 0x012f, 0x00, 0x0e, 0x0000, 0x0000), 0x00), - 'Desert Palace Entrance (East)': (0x09, (0x0085, 0x30, 0x02a8, 0x0c4a, 0x0142, 0x0c98, 0x01c8, 0x0cb7, 0x01cf, 0x06, 0xfe, 0x0000, 0x0000), 0x00), - 'Eastern Palace': (0x07, (0x00c9, 0x1e, 0x005a, 0x0600, 0x0ed6, 0x0618, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000), 0x00), - 'Tower of Hera': (0x32, (0x0077, 0x03, 0x0050, 0x0014, 0x087c, 0x0068, 0x08f0, 0x0083, 0x08fb, 0x0a, 0xf4, 0x0000, 0x0000), 0x00), - 'Hyrule Castle Entrance (South)': (0x03, (0x0061, 0x1b, 0x0530, 0x0692, 0x0784, 0x06cc, 0x07f8, 0x06ff, 0x0803, 0x0e, 0xfa, 0x0000, 0x87be), 0x00), - 'Hyrule Castle Entrance (West)': (0x02, (0x0060, 0x1b, 0x0016, 0x0600, 0x06ae, 0x0604, 0x0728, 0x066d, 0x0733, 0x00, 0x02, 0x0000, 0x8124), 0x00), - 'Hyrule Castle Entrance (East)': (0x04, (0x0062, 0x1b, 0x004a, 0x0600, 0x0856, 0x0604, 0x08c8, 0x066d, 0x08d3, 0x00, 0xfa, 0x0000, 0x8158), 0x00), - 'Inverted Pyramid Entrance': (0x35, (0x0010, 0x1b, 0x000e, 0x0600, 0x0676, 0x0604, 0x06e8, 0x066d, 0x06f3, 0x00, 0x0a, 0x0000, 0x811c), 0x00), - 'Agahnims Tower': (0x23, (0x00e0, 0x1b, 0x0032, 0x0600, 0x0784, 0x0634, 0x07f8, 0x066d, 0x0803, 0x00, 0x0a, 0x0000, 0x82be), 0x40), - 'Thieves Town': (0x33, (0x00db, 0x58, 0x0b2e, 0x075a, 0x0176, 0x07a8, 0x01f8, 0x07c7, 0x0203, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Skull Woods First Section Door': (0x29, (0x0058, 0x40, 0x0f4c, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0xfe, 0x0000, 0x0000), 0x00), - 'Skull Woods Second Section Door (East)': (0x28, (0x0057, 0x40, 0x0eb8, 0x01e6, 0x01c2, 0x0238, 0x0248, 0x0253, 0x024f, 0x0a, 0xfe, 0x0000, 0x0000), 0x00), - 'Skull Woods Second Section Door (West)': (0x27, (0x0056, 0x40, 0x0c8e, 0x01a6, 0x0062, 0x01f8, 0x00e8, 0x0213, 0x00ef, 0x0a, 0x0e, 0x0000, 0x0000), 0x00), - 'Skull Woods Final Section': (0x2A, (0x0059, 0x40, 0x0282, 0x0066, 0x0016, 0x00b8, 0x0098, 0x00d3, 0x00a3, 0x0a, 0xfa, 0x0000, 0x0000), 0x20), - 'Ice Palace': (0x2C, (0x000e, 0x75, 0x0bc6, 0x0d6a, 0x0c3e, 0x0db8, 0x0cb8, 0x0dd7, 0x0cc3, 0x06, 0xf2, 0x0000, 0x0000), 0x00), - 'Misery Mire': (0x26, (0x0098, 0x70, 0x0414, 0x0c79, 0x00a6, 0x0cc7, 0x0128, 0x0ce6, 0x0133, 0x07, 0xfa, 0x0000, 0x0000), 0x20), - 'Palace of Darkness': (0x25, (0x004a, 0x5e, 0x005a, 0x0600, 0x0ed6, 0x0628, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000), 0x20), - 'Swamp Palace': (0x24, (0x0028, 0x7b, 0x049e, 0x0e8c, 0x06f2, 0x0ed8, 0x0778, 0x0ef9, 0x077f, 0x04, 0xfe, 0x0000, 0x0000), 0x00), - 'Turtle Rock': (0x34, (0x00d6, 0x47, 0x0712, 0x00da, 0x0e96, 0x0128, 0x0f08, 0x0147, 0x0f13, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Dark Death Mountain Ledge (West)': (0x14, (0x0023, 0x45, 0x07ca, 0x0103, 0x0c46, 0x0157, 0x0cb8, 0x0172, 0x0cc3, 0x0b, 0x0a, 0x0000, 0x0000), 0x00), - 'Dark Death Mountain Ledge (East)': (0x18, (0x0024, 0x45, 0x07e0, 0x0103, 0x0d00, 0x0157, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Turtle Rock Isolated Ledge Entrance': (0x17, (0x00d5, 0x45, 0x0ad4, 0x0164, 0x0ca6, 0x01b8, 0x0d18, 0x01d3, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), - 'Hyrule Castle Secret Entrance Stairs': (0x31, (0x0055, 0x1b, 0x044a, 0x067a, 0x0854, 0x06c8, 0x08c8, 0x06e7, 0x08d3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Kakariko Well Cave': (0x38, (0x002f, 0x18, 0x0386, 0x0665, 0x0032, 0x06b7, 0x00b8, 0x06d2, 0x00bf, 0x0b, 0xfe, 0x0000, 0x0000), 0x00), - 'Bat Cave Cave': (0x10, (0x00e3, 0x22, 0x0412, 0x087a, 0x048e, 0x08c8, 0x0508, 0x08e7, 0x0513, 0x06, 0x02, 0x0000, 0x0000), 0x00), - 'Elder House (East)': (0x0D, (0x00f3, 0x18, 0x02c4, 0x064a, 0x0222, 0x0698, 0x02a8, 0x06b7, 0x02af, 0x06, 0xfe, 0x05d4, 0x0000), 0x00), - 'Elder House (West)': (0x0C, (0x00f2, 0x18, 0x02bc, 0x064c, 0x01e2, 0x0698, 0x0268, 0x06b9, 0x026f, 0x04, 0xfe, 0x05cc, 0x0000), 0x00), - 'North Fairy Cave': (0x37, (0x0008, 0x15, 0x0088, 0x0400, 0x0a36, 0x0448, 0x0aa8, 0x046f, 0x0ab3, 0x00, 0x0a, 0x0000, 0x0000), 0x00), - 'Lost Woods Hideout Stump': (0x2B, (0x00e1, 0x00, 0x0f4e, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0x0e, 0x0000, 0x0000), 0x00), - 'Lumberjack Tree Cave': (0x11, (0x00e2, 0x02, 0x0118, 0x0015, 0x04c6, 0x0067, 0x0548, 0x0082, 0x0553, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), - 'Two Brothers House (East)': (0x0F, (0x00f5, 0x29, 0x0880, 0x0b07, 0x0200, 0x0b58, 0x0238, 0x0b74, 0x028d, 0x09, 0x00, 0x0b86, 0x0000), 0x00), - 'Two Brothers House (West)': (0x0E, (0x00f4, 0x28, 0x08a0, 0x0b06, 0x0100, 0x0b58, 0x01b8, 0x0b73, 0x018d, 0x0a, 0x00, 0x0bb6, 0x0000), 0x00), - 'Sanctuary': (0x01, (0x0012, 0x13, 0x001c, 0x0400, 0x06de, 0x0414, 0x0758, 0x046d, 0x0763, 0x00, 0x02, 0x0000, 0x01aa), 0x00), - 'Old Man Cave (West)': (0x05, (0x00f0, 0x0a, 0x03a0, 0x0264, 0x0500, 0x02b8, 0x05a8, 0x02d3, 0x058d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Old Man Cave (East)': (0x06, (0x00f1, 0x03, 0x1402, 0x0294, 0x0604, 0x02e8, 0x0678, 0x0303, 0x0683, 0x0a, 0xfc, 0x0000, 0x0000), 0x00), - 'Old Man House (Bottom)': (0x2F, (0x00e4, 0x03, 0x181a, 0x031e, 0x06b4, 0x03a7, 0x0728, 0x038d, 0x0733, 0x00, 0x0c, 0x0000, 0x0000), 0x00), - 'Old Man House (Top)': (0x30, (0x00e5, 0x03, 0x10c6, 0x0224, 0x0814, 0x0278, 0x0888, 0x0293, 0x0893, 0x0a, 0x0c, 0x0000, 0x0000), 0x00), - 'Death Mountain Return Cave (East)': (0x2E, (0x00e7, 0x03, 0x0d82, 0x01c4, 0x0600, 0x0218, 0x0648, 0x0233, 0x067f, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Death Mountain Return Cave (West)': (0x2D, (0x00e6, 0x0a, 0x00a0, 0x0205, 0x0500, 0x0257, 0x05b8, 0x0272, 0x058d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Spectacle Rock Cave Peak': (0x22, (0x00ea, 0x03, 0x092c, 0x0133, 0x0754, 0x0187, 0x07c8, 0x01a2, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), - 'Spectacle Rock Cave': (0x21, (0x00fa, 0x03, 0x0eac, 0x01e3, 0x0754, 0x0237, 0x07c8, 0x0252, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), - 'Spectacle Rock Cave (Bottom)': (0x20, (0x00f9, 0x03, 0x0d9c, 0x01c3, 0x06d4, 0x0217, 0x0748, 0x0232, 0x0753, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), - 'Paradox Cave (Bottom)': (0x1D, (0x00ff, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0237, 0x0da8, 0x0252, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Paradox Cave (Middle)': (0x1E, (0x00ef, 0x05, 0x17e0, 0x0304, 0x0d00, 0x0358, 0x0dc8, 0x0373, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Paradox Cave (Top)': (0x1F, (0x00df, 0x05, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Fairy Ascension Cave (Bottom)': (0x19, (0x00fd, 0x05, 0x0dd4, 0x01c4, 0x0ca6, 0x0218, 0x0d18, 0x0233, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), - 'Fairy Ascension Cave (Top)': (0x1A, (0x00ed, 0x05, 0x0ad4, 0x0163, 0x0ca6, 0x01b7, 0x0d18, 0x01d2, 0x0d23, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), - 'Spiral Cave': (0x1C, (0x00ee, 0x05, 0x07c8, 0x0108, 0x0c46, 0x0158, 0x0cb8, 0x0177, 0x0cc3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Spiral Cave (Bottom)': (0x1B, (0x00fe, 0x05, 0x0cca, 0x01a3, 0x0c56, 0x01f7, 0x0cc8, 0x0212, 0x0cd3, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), - 'Bumper Cave (Bottom)': (0x15, (0x00fb, 0x4a, 0x03a0, 0x0263, 0x0500, 0x02b7, 0x05a8, 0x02d2, 0x058d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Bumper Cave (Top)': (0x16, (0x00eb, 0x4a, 0x00a0, 0x020a, 0x0500, 0x0258, 0x05b8, 0x0277, 0x058d, 0x06, 0x00, 0x0000, 0x0000), 0x00), - 'Superbunny Cave (Top)': (0x13, (0x00e8, 0x45, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Superbunny Cave (Bottom)': (0x12, (0x00f8, 0x45, 0x0ee0, 0x01e4, 0x0d00, 0x0238, 0x0d78, 0x0253, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Hookshot Cave': (0x39, (0x003c, 0x45, 0x04da, 0x00a3, 0x0cd6, 0x0107, 0x0d48, 0x0112, 0x0d53, 0x0b, 0xfa, 0x0000, 0x0000), 0x20), - 'Hookshot Cave Back Entrance': (0x3A, (0x002c, 0x45, 0x004c, 0x0000, 0x0c56, 0x0038, 0x0cc8, 0x006f, 0x0cd3, 0x00, 0x0a, 0x0000, 0x0000), 0x00), - 'Ganons Tower': (0x36, (0x000c, 0x43, 0x0052, 0x0000, 0x0884, 0x0028, 0x08f8, 0x006f, 0x0903, 0x00, 0xfc, 0x0000, 0x0000), 0x20), - 'Pyramid Entrance': (0x35, (0x0010, 0x5b, 0x0b0e, 0x075a, 0x0674, 0x07a8, 0x06e8, 0x07c7, 0x06f3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Skull Woods First Section Hole (West)': ([0xDB84D, 0xDB84E], None), - 'Skull Woods First Section Hole (East)': ([0xDB84F, 0xDB850], None), - 'Skull Woods First Section Hole (North)': ([0xDB84C], None), - 'Skull Woods Second Section Hole': ([0xDB851, 0xDB852], None), - 'Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856], None), - 'Inverted Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856, 0x180340], None), - 'Waterfall of Wishing': (0x5B, (0x0114, 0x0f, 0x0080, 0x0200, 0x0e00, 0x0207, 0x0e60, 0x026f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Dam': (0x4D, (0x010b, 0x3b, 0x04a0, 0x0e8a, 0x06fa, 0x0ed8, 0x0778, 0x0ef7, 0x077f, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Blinds Hideout': (0x60, (0x0119, 0x18, 0x02b2, 0x064a, 0x0186, 0x0697, 0x0208, 0x06b7, 0x0213, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Hyrule Castle Secret Entrance Drop': ([0xDB858], None), - 'Bonk Fairy (Light)': (0x76, (0x0126, 0x2b, 0x00a0, 0x0a0a, 0x0700, 0x0a67, 0x0788, 0x0a77, 0x0785, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Lake Hylia Fairy': (0x5D, (0x0115, 0x2e, 0x0016, 0x0a00, 0x0cb6, 0x0a37, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), - 'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000), 0x20), - 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), - 'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000), 0x00), - 'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000), 0x00), - 'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Lake Hylia Shop': (0x57, (0x0112, 0x35, 0x0022, 0x0c00, 0x0b1a, 0x0c26, 0x0b98, 0x0c6d, 0x0b9f, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Capacity Upgrade': (0x5C, (0x0115, 0x35, 0x0a46, 0x0d36, 0x0c2a, 0x0d88, 0x0ca8, 0x0da3, 0x0caf, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Kakariko Well Drop': ([0xDB85C, 0xDB85D], None), - 'Blacksmiths Hut': (0x63, (0x0121, 0x22, 0x010c, 0x081a, 0x0466, 0x0868, 0x04d8, 0x0887, 0x04e3, 0x06, 0xfa, 0x041A, 0x0000), 0x00), - 'Bat Cave Drop': ([0xDB859, 0xDB85A], None), - 'Sick Kids House': (0x3F, (0x0102, 0x18, 0x10be, 0x0826, 0x01f6, 0x0877, 0x0278, 0x0893, 0x0283, 0x08, 0xf8, 0x14CE, 0x0000), 0x00), - 'North Fairy Cave Drop': ([0xDB857], None), - 'Lost Woods Gamble': (0x3B, (0x0100, 0x00, 0x004e, 0x0000, 0x0272, 0x0008, 0x02f0, 0x006f, 0x02f7, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Fortune Teller (Light)': (0x64, (0x0122, 0x11, 0x060e, 0x04b4, 0x027d, 0x0508, 0x02f8, 0x0523, 0x0302, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Snitch Lady (East)': (0x3D, (0x0101, 0x18, 0x0ad8, 0x074a, 0x02c6, 0x0798, 0x0348, 0x07b7, 0x0353, 0x06, 0xfa, 0x0DE8, 0x0000), 0x00), - 'Snitch Lady (West)': (0x3E, (0x0101, 0x18, 0x0788, 0x0706, 0x0046, 0x0758, 0x00c8, 0x0773, 0x00d3, 0x08, 0xf8, 0x0B98, 0x0000), 0x00), - 'Bush Covered House': (0x43, (0x0103, 0x18, 0x1156, 0x081a, 0x02b6, 0x0868, 0x0338, 0x0887, 0x0343, 0x06, 0xfa, 0x1466, 0x0000), 0x00), - 'Tavern (Front)': (0x41, (0x0103, 0x18, 0x1842, 0x0916, 0x0206, 0x0967, 0x0288, 0x0983, 0x0293, 0x08, 0xf8, 0x1C50, 0x0000), 0x00), - 'Light World Bomb Hut': (0x49, (0x0107, 0x18, 0x1800, 0x0916, 0x0000, 0x0967, 0x0068, 0x0983, 0x008d, 0x08, 0xf8, 0x9C0C, 0x0000), 0x02), - 'Kakariko Shop': (0x45, (0x011f, 0x18, 0x16a8, 0x08e7, 0x0136, 0x0937, 0x01b8, 0x0954, 0x01c3, 0x07, 0xf9, 0x1AB6, 0x0000), 0x00), - 'Lost Woods Hideout Drop': ([0xDB853], None), - 'Lumberjack Tree Tree': ([0xDB85B], None), - 'Cave 45': (0x50, (0x011b, 0x32, 0x0680, 0x0cc9, 0x0400, 0x0d16, 0x0438, 0x0d36, 0x0485, 0x07, 0xf9, 0x0000, 0x0000), 0x00), - 'Graveyard Cave': (0x51, (0x011b, 0x14, 0x0016, 0x0400, 0x08a2, 0x0446, 0x0918, 0x046d, 0x091f, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Checkerboard Cave': (0x7D, (0x0126, 0x30, 0x00c8, 0x0c0a, 0x024a, 0x0c67, 0x02c8, 0x0c77, 0x02cf, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Mini Moldorm Cave': (0x7C, (0x0123, 0x35, 0x1480, 0x0e96, 0x0a00, 0x0ee8, 0x0a68, 0x0f03, 0x0a85, 0x08, 0xf8, 0x0000, 0x0000), 0x02), - 'Long Fairy Cave': (0x54, (0x011e, 0x2f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Good Bee Cave': (0x6A, (0x0120, 0x37, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000), 0x00), - '20 Rupee Cave': (0x7A, (0x0125, 0x37, 0x0200, 0x0c23, 0x0e00, 0x0c86, 0x0e68, 0x0c92, 0x0e7d, 0x0d, 0xf3, 0x0000, 0x0000), 0x20), - '50 Rupee Cave': (0x78, (0x0124, 0x3a, 0x0790, 0x0eea, 0x047a, 0x0f47, 0x04f8, 0x0f57, 0x04ff, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Ice Rod Cave': (0x7F, (0x0120, 0x37, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x02), - 'Bonk Rock Cave': (0x79, (0x0124, 0x13, 0x0280, 0x044a, 0x0600, 0x04a7, 0x0638, 0x04b7, 0x067d, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Library': (0x48, (0x0107, 0x29, 0x0100, 0x0a14, 0x0200, 0x0a67, 0x0278, 0x0a83, 0x0285, 0x0a, 0xf6, 0x040E, 0x0000), 0x00), - 'Potion Shop': (0x4B, (0x0109, 0x16, 0x070a, 0x04e6, 0x0c56, 0x0538, 0x0cc8, 0x0553, 0x0cd3, 0x08, 0xf8, 0x0A98, 0x0000), 0x00), - 'Sanctuary Grave': ([0xDB85E], None), - 'Hookshot Fairy': (0x4F, (0x010c, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0d78, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), - 'Pyramid Fairy': (0x62, (0x0116, 0x5b, 0x0b1e, 0x0754, 0x06fa, 0x07a7, 0x0778, 0x07c3, 0x077f, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), - 'East Dark World Hint': (0x68, (0x010e, 0x6f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Palace of Darkness Hint': (0x67, (0x011a, 0x5e, 0x0c24, 0x0794, 0x0d12, 0x07e8, 0x0d90, 0x0803, 0x0d97, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Lake Hylia Fairy': (0x6C, (0x0115, 0x6e, 0x0016, 0x0a00, 0x0cb6, 0x0a36, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Dark Lake Hylia Ledge Fairy': (0x80, (0x0115, 0x77, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x02), - 'Dark Lake Hylia Ledge Spike Cave': (0x7B, (0x0125, 0x77, 0x0200, 0x0c27, 0x0e00, 0x0c86, 0x0e68, 0x0c96, 0x0e7d, 0x09, 0xf7, 0x0000, 0x0000), 0x20), - 'Dark Lake Hylia Ledge Hint': (0x69, (0x010e, 0x77, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Hype Cave': (0x3C, (0x011e, 0x74, 0x00a0, 0x0c0a, 0x0900, 0x0c58, 0x0988, 0x0c77, 0x097d, 0x06, 0xfa, 0x0000, 0x0000), 0x02), - 'Bonk Fairy (Dark)': (0x77, (0x0126, 0x6b, 0x00a0, 0x0a05, 0x0700, 0x0a66, 0x0788, 0x0a72, 0x0785, 0x0b, 0xf5, 0x0000, 0x0000), 0x20), - 'Brewery': (0x47, (0x0106, 0x58, 0x16a8, 0x08e4, 0x013e, 0x0938, 0x01b8, 0x0953, 0x01c3, 0x0a, 0xf6, 0x1AB6, 0x0000), 0x02), - 'C-Shaped House': (0x53, (0x011c, 0x58, 0x09d8, 0x0744, 0x02ce, 0x0797, 0x0348, 0x07b3, 0x0353, 0x0a, 0xf6, 0x0DE8, 0x0000), 0x00), - 'Chest Game': (0x46, (0x0106, 0x58, 0x078a, 0x0705, 0x004e, 0x0758, 0x00c8, 0x0774, 0x00d3, 0x09, 0xf7, 0x0B98, 0x0000), 0x00), - 'Hammer Peg Cave': (0x7E, (0x0127, 0x62, 0x0894, 0x091e, 0x0492, 0x09a6, 0x0508, 0x098b, 0x050f, 0x00, 0x00, 0x0000, 0x0000), 0x20), - 'Red Shield Shop': (0x74, (0x0110, 0x5a, 0x079a, 0x06e8, 0x04d6, 0x0738, 0x0548, 0x0755, 0x0553, 0x08, 0xf8, 0x0AA8, 0x0000), 0x00), - 'Dark Sanctuary Hint': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Fortune Teller (Dark)': (0x65, (0x0122, 0x51, 0x0610, 0x04b4, 0x027e, 0x0507, 0x02f8, 0x0523, 0x0303, 0x0a, 0xf6, 0x091E, 0x0000), 0x00), - 'Dark World Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Lumberjack Shop': (0x56, (0x010f, 0x42, 0x041c, 0x0074, 0x04e2, 0x00c7, 0x0558, 0x00e3, 0x055f, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Potion Shop': (0x6E, (0x010f, 0x56, 0x080e, 0x04f4, 0x0c66, 0x0548, 0x0cd8, 0x0563, 0x0ce3, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Archery Game': (0x58, (0x0111, 0x69, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00), - 'Mire Shed': (0x5E, (0x010d, 0x70, 0x0384, 0x0c69, 0x001e, 0x0cb6, 0x0098, 0x0cd6, 0x00a3, 0x07, 0xf9, 0x0000, 0x0000), 0x00), - 'Mire Hint': (0x61, (0x0114, 0x70, 0x0654, 0x0cc5, 0x02aa, 0x0d16, 0x0328, 0x0d32, 0x032f, 0x09, 0xf7, 0x0000, 0x0000), 0x00), - 'Mire Fairy': (0x55, (0x0115, 0x70, 0x03a8, 0x0c6a, 0x013a, 0x0cb7, 0x01b8, 0x0cd7, 0x01bf, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Spike Cave': (0x40, (0x0117, 0x43, 0x0ed4, 0x01e4, 0x08aa, 0x0236, 0x0928, 0x0253, 0x092f, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Death Mountain Shop': (0x6D, (0x0112, 0x45, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0daa, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), - 'Dark Death Mountain Fairy': (0x6F, (0x0115, 0x43, 0x1400, 0x0294, 0x0600, 0x02e8, 0x0678, 0x0303, 0x0685, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Mimic Cave': (0x4E, (0x010c, 0x05, 0x07e0, 0x0103, 0x0d00, 0x0156, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), - 'Big Bomb Shop': (0x52, (0x011c, 0x6c, 0x0506, 0x0a9a, 0x0832, 0x0ae7, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfa, 0x0816, 0x0000), 0x00), - 'Dark Lake Hylia Shop': (0x73, (0x010f, 0x75, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Lumberjack House': (0x75, (0x011f, 0x02, 0x049c, 0x0088, 0x04e6, 0x00d8, 0x0558, 0x00f7, 0x0563, 0x08, 0xf8, 0x07AA, 0x0000), 0x00), - 'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)} - -# format: -# Key=Name -# value = entrance # -# | (entrance #, exit #) -exit_ids = {'Links House Exit': (0x01, 0x00), - 'Chris Houlihan Room Exit': (None, 0x3D), - 'Desert Palace Exit (South)': (0x09, 0x0A), - 'Desert Palace Exit (West)': (0x0B, 0x0C), - 'Desert Palace Exit (East)': (0x0A, 0x0B), - 'Desert Palace Exit (North)': (0x0C, 0x0D), - 'Eastern Palace Exit': (0x08, 0x09), - 'Tower of Hera Exit': (0x33, 0x2D), - 'Hyrule Castle Exit (South)': (0x04, 0x03), - 'Hyrule Castle Exit (West)': (0x03, 0x02), - 'Hyrule Castle Exit (East)': (0x05, 0x04), - 'Agahnims Tower Exit': (0x24, 0x25), - 'Thieves Town Exit': (0x34, 0x35), - 'Skull Woods First Section Exit': (0x2A, 0x2B), - 'Skull Woods Second Section Exit (East)': (0x29, 0x2A), - 'Skull Woods Second Section Exit (West)': (0x28, 0x29), - 'Skull Woods Final Section Exit': (0x2B, 0x2C), - 'Ice Palace Exit': (0x2D, 0x2E), - 'Misery Mire Exit': (0x27, 0x28), - 'Palace of Darkness Exit': (0x26, 0x27), - 'Swamp Palace Exit': (0x25, 0x26), - 'Turtle Rock Exit (Front)': (0x35, 0x34), - 'Turtle Rock Ledge Exit (West)': (0x15, 0x16), - 'Turtle Rock Ledge Exit (East)': (0x19, 0x1A), - 'Turtle Rock Isolated Ledge Exit': (0x18, 0x19), - 'Hyrule Castle Secret Entrance Exit': (0x32, 0x33), - 'Kakariko Well Exit': (0x39, 0x3A), - 'Bat Cave Exit': (0x11, 0x12), - 'Elder House Exit (East)': (0x0E, 0x0F), - 'Elder House Exit (West)': (0x0D, 0x0E), - 'North Fairy Cave Exit': (0x38, 0x39), - 'Lost Woods Hideout Exit': (0x2C, 0x36), - 'Lumberjack Tree Exit': (0x12, 0x13), - 'Two Brothers House Exit (East)': (0x10, 0x11), - 'Two Brothers House Exit (West)': (0x0F, 0x10), - 'Sanctuary Exit': (0x02, 0x01), - 'Old Man Cave Exit (East)': (0x07, 0x08), - 'Old Man Cave Exit (West)': (0x06, 0x07), - 'Old Man House Exit (Bottom)': (0x30, 0x31), - 'Old Man House Exit (Top)': (0x31, 0x32), - 'Death Mountain Return Cave Exit (West)': (0x2E, 0x2F), - 'Death Mountain Return Cave Exit (East)': (0x2F, 0x30), - 'Spectacle Rock Cave Exit': (0x21, 0x22), - 'Spectacle Rock Cave Exit (Top)': (0x22, 0x23), - 'Spectacle Rock Cave Exit (Peak)': (0x23, 0x24), - 'Paradox Cave Exit (Bottom)': (0x1E, 0x1F), - 'Paradox Cave Exit (Middle)': (0x1F, 0x20), - 'Paradox Cave Exit (Top)': (0x20, 0x21), - 'Fairy Ascension Cave Exit (Bottom)': (0x1A, 0x1B), - 'Fairy Ascension Cave Exit (Top)': (0x1B, 0x1C), - 'Spiral Cave Exit': (0x1C, 0x1D), - 'Spiral Cave Exit (Top)': (0x1D, 0x1E), - 'Bumper Cave Exit (Top)': (0x17, 0x18), - 'Bumper Cave Exit (Bottom)': (0x16, 0x17), - 'Superbunny Cave Exit (Top)': (0x14, 0x15), - 'Superbunny Cave Exit (Bottom)': (0x13, 0x14), - 'Hookshot Cave Front Exit': (0x3A, 0x3B), - 'Hookshot Cave Back Exit': (0x3B, 0x3C), - 'Ganons Tower Exit': (0x37, 0x38), - 'Pyramid Exit': (0x36, 0x37), - 'Waterfall of Wishing': 0x5C, - 'Dam': 0x4E, - 'Blinds Hideout': 0x61, - 'Lumberjack House': 0x6B, - 'Bonk Fairy (Light)': 0x71, - 'Bonk Fairy (Dark)': 0x71, - 'Lake Hylia Healer Fairy': 0x5E, - 'Light Hype Fairy': 0x5E, - 'Desert Healer Fairy': 0x5E, - 'Dark Lake Hylia Healer Fairy': 0x5E, - 'Dark Lake Hylia Ledge Healer Fairy': 0x5E, - 'Mire Healer Fairy': 0x5E, - 'Dark Death Mountain Healer Fairy': 0x5E, - 'Fortune Teller (Light)': 0x65, - 'Lake Hylia Fortune Teller': 0x65, - 'Kings Grave': 0x5B, - 'Tavern': 0x43, - 'Chicken House': 0x4B, - 'Aginahs Cave': 0x4D, - 'Sahasrahlas Hut': 0x45, - 'Lake Hylia Shop': 0x58, - 'Dark Death Mountain Shop': 0x58, - 'Capacity Upgrade': 0x5D, - 'Blacksmiths Hut': 0x64, - 'Sick Kids House': 0x40, - 'Lost Woods Gamble': 0x3C, - 'Snitch Lady (East)': 0x3E, - 'Snitch Lady (West)': 0x3F, - 'Bush Covered House': 0x44, - 'Tavern (Front)': 0x42, - 'Light World Bomb Hut': 0x4A, - 'Kakariko Shop': 0x46, - 'Cave 45': 0x51, - 'Graveyard Cave': 0x52, - 'Checkerboard Cave': 0x72, - 'Mini Moldorm Cave': 0x6C, - 'Long Fairy Cave': 0x55, - 'Good Bee Cave': 0x56, - '20 Rupee Cave': 0x6F, - '50 Rupee Cave': 0x6D, - 'Ice Rod Cave': 0x84, - 'Bonk Rock Cave': 0x6E, - 'Library': 0x49, - 'Kakariko Gamble Game': 0x67, - 'Potion Shop': 0x4C, - 'Hookshot Fairy': 0x50, - 'Pyramid Fairy': 0x63, - 'East Dark World Hint': 0x69, - 'Palace of Darkness Hint': 0x68, - 'Big Bomb Shop': 0x53, - 'Village of Outcasts Shop': 0x60, - 'Dark Lake Hylia Shop': 0x60, - 'Dark Lumberjack Shop': 0x60, - 'Dark Potion Shop': 0x60, - 'Dark Lake Hylia Ledge Spike Cave': 0x70, - 'Dark Lake Hylia Ledge Hint': 0x6A, - 'Hype Cave': 0x3D, - 'Brewery': 0x48, - 'C-Shaped House': 0x54, - 'Chest Game': 0x47, - 'Hammer Peg Cave': 0x83, - 'Red Shield Shop': 0x57, - 'Dark Sanctuary Hint': 0x5A, - 'Fortune Teller (Dark)': 0x66, - 'Archery Game': 0x59, - 'Mire Shed': 0x5F, - 'Mire Hint': 0x62, - 'Spike Cave': 0x41, - 'Mimic Cave': 0x4F, - 'Kakariko Well (top)': 0x80, - 'Hyrule Castle Secret Entrance': 0x7D, - 'Bat Cave (right)': 0x7E, - 'North Fairy Cave': 0x7C, - 'Lost Woods Hideout (top)': 0x7A, - 'Lumberjack Tree (top)': 0x7F, - 'Sewer Drop': 0x81, - 'Skull Back Drop': 0x79, - 'Skull Left Drop': 0x77, - 'Skull Pinball': 0x78, - 'Skull Pot Circle': 0x76, - 'Pyramid': 0x7B} - -ow_prize_table = {'Links House': (0x8b1, 0xb2d), - 'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0), - 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), - 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), - 'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0), - 'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0), 'Inverted Pyramid Entrance': (0x6C0, 0x5D0), - 'Agahnims Tower': (0x820, 0x5D0), - 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x240, 0x280), - 'Skull Woods Second Section Door (East)': (0x1a0, 0x240), - 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), 'Skull Woods Final Section': (0x082, 0x0b0), - 'Ice Palace': (0xca0, 0xda0), - 'Misery Mire': (0x100, 0xca0), - 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), - 'Turtle Rock': (0xf11, 0x103), - 'Dark Death Mountain Ledge (West)': (0xb80, 0x180), - 'Dark Death Mountain Ledge (East)': (0xc80, 0x180), - 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), - 'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700), - 'Kakariko Well Cave': (0x060, 0x680), - 'Bat Cave Cave': (0x540, 0x8f0), - 'Elder House (East)': (0x2b0, 0x6a0), - 'Elder House (West)': (0x230, 0x6a0), - 'North Fairy Cave': (0xa80, 0x440), - 'Lost Woods Hideout Stump': (0x240, 0x280), - 'Lumberjack Tree Cave': (0x4e0, 0x004), - 'Two Brothers House (East)': (0x200, 0x0b60), - 'Two Brothers House (West)': (0x180, 0x0b60), - 'Sanctuary': (0x720, 0x4a0), - 'Old Man Cave (West)': (0x580, 0x2c0), - 'Old Man Cave (East)': (0x620, 0x2c0), - 'Old Man House (Bottom)': (0x720, 0x320), - 'Old Man House (Top)': (0x820, 0x220), - 'Death Mountain Return Cave (East)': (0x600, 0x220), - 'Death Mountain Return Cave (West)': (0x500, 0x1c0), - 'Spectacle Rock Cave Peak': (0x720, 0x0a0), - 'Spectacle Rock Cave': (0x790, 0x1a0), - 'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0), - 'Paradox Cave (Bottom)': (0xd80, 0x180), - 'Paradox Cave (Middle)': (0xd80, 0x380), - 'Paradox Cave (Top)': (0xd80, 0x020), - 'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0), - 'Fairy Ascension Cave (Top)': (0xc00, 0x240), - 'Spiral Cave': (0xb80, 0x180), - 'Spiral Cave (Bottom)': (0xb80, 0x2c0), - 'Bumper Cave (Bottom)': (0x580, 0x2c0), - 'Bumper Cave (Top)': (0x500, 0x1c0), - 'Superbunny Cave (Top)': (0xd80, 0x020), - 'Superbunny Cave (Bottom)': (0xd00, 0x180), - 'Hookshot Cave': (0xc80, 0x0c0), - 'Hookshot Cave Back Entrance': (0xcf0, 0x004), - 'Ganons Tower': (0x8D0, 0x080), - 'Pyramid Entrance': (0x640, 0x7c0), - 'Skull Woods First Section Hole (West)': None, - 'Skull Woods First Section Hole (East)': None, - 'Skull Woods First Section Hole (North)': None, - 'Skull Woods Second Section Hole': None, - 'Pyramid Hole': None, - 'Inverted Pyramid Hole': None, - 'Waterfall of Wishing': (0xe80, 0x280), - 'Dam': (0x759, 0xED0), - 'Blinds Hideout': (0x190, 0x6c0), - 'Hyrule Castle Secret Entrance Drop': None, - 'Bonk Fairy (Light)': (0x740, 0xa80), - 'Lake Hylia Fairy': (0xd40, 0x9f0), - 'Light Hype Fairy': (0x940, 0xc80), - 'Desert Fairy': (0x420, 0xe00), - 'Kings Grave': (0x920, 0x520), - 'Tavern North': (0x270, 0x900), - 'Chicken House': (0x120, 0x880), - 'Aginahs Cave': (0x2e0, 0xd00), - 'Sahasrahlas Hut': (0xcf0, 0x6c0), - 'Lake Hylia Shop': (0xbc0, 0xc00), - 'Capacity Upgrade': (0xca0, 0xda0), - 'Kakariko Well Drop': None, - 'Blacksmiths Hut': (0x4a0, 0x880), - 'Bat Cave Drop': None, - 'Sick Kids House': (0x220, 0x880), - 'North Fairy Cave Drop': None, - 'Lost Woods Gamble': (0x240, 0x080), - 'Fortune Teller (Light)': (0x2c0, 0x4c0), - 'Snitch Lady (East)': (0x310, 0x7a0), - 'Snitch Lady (West)': (0x080, 0x7a0), - 'Bush Covered House': (0x2e0, 0x880), - 'Tavern (Front)': (0x270, 0x980), - 'Light World Bomb Hut': (0x070, 0x980), - 'Kakariko Shop': (0x170, 0x980), - 'Lost Woods Hideout Drop': None, - 'Lumberjack Tree Tree': None, - 'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), - 'Checkerboard Cave': (0x260, 0xc00), - 'Mini Moldorm Cave': (0xa40, 0xe80), - 'Long Fairy Cave': (0xf60, 0xb00), - 'Good Bee Cave': (0xec0, 0xc00), - '20 Rupee Cave': (0xe80, 0xca0), - '50 Rupee Cave': (0x4d0, 0xed0), - 'Ice Rod Cave': (0xe00, 0xc00), - 'Bonk Rock Cave': (0x5f0, 0x460), - 'Library': (0x270, 0xaa0), - 'Potion Shop': (0xc80, 0x4c0), - 'Sanctuary Grave': None, - 'Hookshot Fairy': (0xd00, 0x180), - 'Pyramid Fairy': (0x740, 0x740), - 'East Dark World Hint': (0xf60, 0xb00), - 'Palace of Darkness Hint': (0xd60, 0x7c0), - 'Dark Lake Hylia Fairy': (0xd40, 0x9f0), - 'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00), - 'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0), - 'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00), - 'Hype Cave': (0x940, 0xc80), - 'Bonk Fairy (Dark)': (0x740, 0xa80), - 'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x080, 0x7a0), - 'Hammer Peg Cave': (0x4c0, 0x940), - 'Red Shield Shop': (0x500, 0x680), - 'Dark Sanctuary Hint': (0x720, 0x4a0), - 'Fortune Teller (Dark)': (0x2c0, 0x4c0), - 'Dark World Shop': (0x2e0, 0x880), - 'Dark Lumberjack Shop': (0x4e0, 0x0d0), - 'Dark Potion Shop': (0xc80, 0x4c0), - 'Archery Game': (0x2f0, 0xaf0), - 'Mire Shed': (0x060, 0xc90), - 'Mire Hint': (0x2e0, 0xd00), - 'Mire Fairy': (0x1c0, 0xc90), - 'Spike Cave': (0x860, 0x180), - 'Dark Death Mountain Shop': (0xd80, 0x180), - 'Dark Death Mountain Fairy': (0x620, 0x2c0), - 'Mimic Cave': (0xc80, 0x180), - 'Big Bomb Shop': (0x8b1, 0xb2d), - 'Dark Lake Hylia Shop': (0xa40, 0xc40), - 'Lumberjack House': (0x4e0, 0x0d0), - 'Lake Hylia Fortune Teller': (0xa40, 0xc40), - 'Kakariko Gamble Game': (0x2f0, 0xaf0)} diff --git a/Fill.py b/Fill.py index b7e4b7cf..8a917070 100644 --- a/Fill.py +++ b/Fill.py @@ -14,14 +14,19 @@ from source.item.FillUtil import filter_special_locations, valid_pot_items def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None] + dungeon_items = [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None] + for player in range(1, world.players+1): + if world.prizeshuffle[player] != 'none': + dungeon_items.extend(ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)) + + return dungeon_items def promote_dungeon_items(world): world.itempool += get_dungeon_item_pool(world) for item in world.get_items(): - if item.smallkey or item.bigkey: + if item.smallkey or item.bigkey or item.prize: item.advancement = True elif item.map or item.compass: item.priority = True @@ -38,36 +43,62 @@ def fill_dungeons_restrictive(world, shuffled_locations): # with shuffled dungeon items they are distributed as part of the normal item pool for item in world.get_items(): - if ((item.smallkey and world.keyshuffle[item.player] != 'none') + if ((item.prize and world.prizeshuffle[item.player] != 'none') + or (item.smallkey and world.keyshuffle[item.player] != 'none') or (item.bigkey and world.bigkeyshuffle[item.player])): item.advancement = True elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): item.priority = True dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - bigs, smalls, others = [], [], [] + bigs, smalls, prizes, others = [], [], [], [] for i in dungeon_items: - (bigs if i.bigkey else smalls if i.smallkey else others).append(i) + (bigs if i.bigkey else smalls if i.smallkey else prizes if i.prize else others).append(i) unplaced_smalls = list(smalls) for i in world.itempool: if i.smallkey and world.keyshuffle[i.player] != 'none': unplaced_smalls.append(i) - def fill(base_state, items, key_pool): - fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) + def fill(base_state, items, locations, key_pool=None): + fill_restrictive(world, base_state, locations, items, key_pool, True) all_state_base = world.get_all_state() big_state_base = all_state_base.copy() - for x in smalls + others: + for x in smalls + prizes + others: big_state_base.collect(x, True) - fill(big_state_base, bigs, unplaced_smalls) + fill(big_state_base, bigs, shuffled_locations, unplaced_smalls) random.shuffle(shuffled_locations) small_state_base = all_state_base.copy() - for x in others: + for x in prizes + others: small_state_base.collect(x, True) - fill(small_state_base, smalls, unplaced_smalls) + fill(small_state_base, smalls, shuffled_locations, unplaced_smalls) + + prizes_copy = prizes.copy() + for attempt in range(15): + try: + random.shuffle(prizes) + random.shuffle(shuffled_locations) + prize_state_base = all_state_base.copy() + for x in others: + prize_state_base.collect(x, True) + fill(prize_state_base, prizes, shuffled_locations) + except FillError as e: + logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, 14 - attempt) + prizes = prizes_copy.copy() + for dungeon in world.dungeons: + if world.prizeshuffle[dungeon.player] == 'dungeon': + dungeon.prize = None + for prize in prizes: + if prize.location: + prize.location.item = None + prize.location = None + continue + break + else: + raise FillError(f'Unable to place dungeon prizes {", ".join(list(map(lambda d: d.hint_text, prize_locs)))}') + random.shuffle(shuffled_locations) - fill(all_state_base, others, None) + fill(all_state_base, others, shuffled_locations) def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False, @@ -141,7 +172,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, key_pool, world): - if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: # a better test to see if a key can go there location.item = item_to_place location.event = True if item_to_place.smallkey: @@ -155,9 +186,9 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): if valid_key_placement(item_to_place, location, key_pool, test_state, world): - if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): + if item_to_place.prize or valid_dungeon_placement(item_to_place, location, world): return location - if item_to_place.smallkey or item_to_place.bigkey: + if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: location.item = None location.event = False if item_to_place.smallkey: @@ -181,8 +212,8 @@ def valid_key_placement(item, location, key_pool, collection_state, world): key_logic = world.key_logic[item.player][dungeon.name] unplaced_keys = len([x for x in key_pool if x.name == key_logic.small_key_name and x.player == item.player]) prize_loc = None - if key_logic.prize_location: - prize_loc = world.get_location(key_logic.prize_location, location.player) + if key_logic.prize_location and dungeon.prize and dungeon.prize.location and dungeon.prize.location.player == item.player: + prize_loc = dungeon.prize.location cr_count = world.crystals_needed_for_gt[location.player] wild_keys = world.keyshuffle[item.player] != 'none' if wild_keys: @@ -193,7 +224,7 @@ def valid_key_placement(item, location, key_pool, collection_state, world): self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name) return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys, - location if item.bigkey else None, prize_loc, cr_count) + location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -228,16 +259,19 @@ def track_outside_keys(item, location, world): def track_dungeon_items(item, location, world): - if location.parent_region.dungeon and not item.crystal: + if location.parent_region.dungeon and (not item.prize or world.prizeshuffle[item.player] == 'dungeon'): layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] if is_dungeon_item(item, world) and item.player == location.player: layout.dungeon_items -= 1 else: layout.free_items -= 1 + if item.prize: + location.parent_region.dungeon.prize = item def is_dungeon_item(item, world): - return ((item.smallkey and world.keyshuffle[item.player] == 'none') + return ((item.prize and world.prizeshuffle[item.player] in ['none', 'dungeon']) + or (item.smallkey and world.keyshuffle[item.player] == 'none') or (item.bigkey and not world.bigkeyshuffle[item.player]) or (item.compass and not world.compassshuffle[item.player]) or (item.map and not world.mapshuffle[item.player])) @@ -250,8 +284,8 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, key_pool, single_player_placement) elif world.algorithm == 'vanilla_fill': - if item_to_place.type == 'Crystal': - possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + if item_to_place.prize: + possible_swaps = [x for x in state.locations_checked if x.item.prize] return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, key_pool, single_player_placement) else: @@ -310,11 +344,15 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite return 3 return 4 - if item_to_place.type == 'Crystal': - possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + # TODO: Verify correctness in using item player in multiworld situations + if item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none': + possible_swaps = [x for x in state.locations_checked if x.item.prize] else: + ignored_types = ['Event'] + if world.prizeshuffle[item_to_place.player] == 'none': + ignored_types.append('Prize') possible_swaps = [x for x in state.locations_checked - if x.item.type not in ['Event', 'Crystal'] and not x.forced_item and not x.locked] + if x.item.type not in ignored_types and not x.forced_item and not x.locked] swap_locations = sorted(possible_swaps, key=location_preference) return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, key_pool, single_player_placement) @@ -516,7 +554,7 @@ def config_sort(world): if world.item_pool_config.verify: config_sort_helper(world, world.item_pool_config.verify) elif world.item_pool_config.preferred: - config_sort_helper(world, world.item_pool_config.preferred) + config_sort_helper_random(world, world.item_pool_config.preferred) def config_sort_helper(world, sort_dict): @@ -526,6 +564,10 @@ def config_sort_helper(world, sort_dict): if (i.name, i.player) in sort_dict else 0) +def config_sort_helper_random(world, sort_dict): + world.itempool.sort(key=lambda i: 1 if (i.name, i.player) in sort_dict else 0) + + def calc_trash_locations(world, player): total_count, gt_count = 0, 0 for loc in world.get_locations(): @@ -1115,7 +1157,7 @@ def set_prize_drops(world, player): for player, drop_config in drops.items(): for pack_num in range(1, 8): if f'Pack {pack_num}' in drop_config: - for prize, idx in enumerate(drop_config[f'Pack {pack_num}']): + for idx, prize in enumerate(drop_config[f'Pack {pack_num}']): chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize] prizes[(pack_num-1)*8 + idx] = chosen for tree_pull_tier in range(1, 4): diff --git a/Gui.py b/Gui.py index 5155091d..bbb3eada 100755 --- a/Gui.py +++ b/Gui.py @@ -44,7 +44,7 @@ def save_settings(gui, args, filename): if not os.path.exists(settings_path): os.makedirs(settings_path) output_args = {} - settings = ["create_rom", "suppress_rom", "bps", "create_spoiler", "suppress_spoiler", + settings = ["create_rom", "suppress_rom", "bps", "spoiler", "calc_playthrough", "skip_playthrough", "print_custom_yaml", "settingsonload", "rom", "outputpath"] if filename == "settings.json": diff --git a/ItemList.py b/ItemList.py index 16130fc9..a3cf5438 100644 --- a/ItemList.py +++ b/ItemList.py @@ -4,14 +4,14 @@ import math import RaceRandom as random from BaseClasses import LocationType, Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem -from EntranceShuffle import connect_entrance -from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location +from Regions import location_events, shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys from PotShuffle import vanilla_pots from Tables import bonk_prize_lookup from Items import ItemFactory from source.dungeon.EnemyList import add_drop_contents +from source.overworld.EntranceShuffle2 import connect_entrance from source.item.FillUtil import trash_items, pot_items import source.classes.constants as CONST @@ -194,10 +194,17 @@ def generate_itempool(world, player): if world.timer in ['ohko', 'timed-ohko']: world.can_take_damage = False + def set_event_item(location_name, item_name=None): + location = world.get_location(location_name, player) + if item_name: + world.push_item(location, ItemFactory(item_name, player), False) + location.event = True + location.locked = True + if world.goal[player] in ['pedestal', 'triforcehunt']: - world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False) + set_event_item('Ganon', 'Nothing') else: - world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) + set_event_item('Ganon', 'Triforce') if world.goal[player] in ['triforcehunt', 'trinity']: region = world.get_region('Hyrule Castle Courtyard', player) @@ -233,75 +240,13 @@ def generate_itempool(world, player): old_man.forced_item = old_man.item old_man.skip = True - world.get_location('Ganon', player).event = True - world.get_location('Ganon', player).locked = True - world.push_item(world.get_location('Agahnim 1', player), ItemFactory('Beat Agahnim 1', player), False) - world.get_location('Agahnim 1', player).event = True - world.get_location('Agahnim 1', player).locked = True - world.push_item(world.get_location('Agahnim 2', player), ItemFactory('Beat Agahnim 2', player), False) - world.get_location('Agahnim 2', player).event = True - world.get_location('Agahnim 2', player).locked = True - world.push_item(world.get_location('Lost Old Man', player), ItemFactory('Escort Old Man', player), False) - world.get_location('Lost Old Man', player).event = True - world.get_location('Lost Old Man', player).locked = True - world.push_item(world.get_location('Old Man Drop Off', player), ItemFactory('Return Old Man', player), False) - world.get_location('Old Man Drop Off', player).event = True - world.get_location('Old Man Drop Off', player).locked = True - world.push_item(world.get_location('Dark Blacksmith Ruins', player), ItemFactory('Pick Up Purple Chest', player), False) - world.get_location('Dark Blacksmith Ruins', player).event = True - world.get_location('Dark Blacksmith Ruins', player).locked = True - world.push_item(world.get_location('Middle Aged Man', player), ItemFactory('Deliver Purple Chest', player), False) - world.get_location('Middle Aged Man', player).event = True - world.get_location('Middle Aged Man', player).locked = True - world.push_item(world.get_location('Frog', player), ItemFactory('Get Frog', player), False) - world.get_location('Frog', player).event = True - world.get_location('Frog', player).locked = True - world.push_item(world.get_location('Missing Smith', player), ItemFactory('Return Smith', player), False) - world.get_location('Missing Smith', player).event = True - world.get_location('Missing Smith', player).locked = True - world.push_item(world.get_location('Floodgate', player), ItemFactory('Open Floodgate', player), False) - world.get_location('Floodgate', player).event = True - world.get_location('Floodgate', player).locked = True - world.push_item(world.get_location('Big Bomb', player), ItemFactory('Pick Up Big Bomb', player), False) - world.get_location('Big Bomb', player).event = True - world.get_location('Big Bomb', player).locked = True - world.push_item(world.get_location('Pyramid Crack', player), ItemFactory('Detonate Big Bomb', player), False) - world.get_location('Pyramid Crack', player).event = True - world.get_location('Pyramid Crack', player).locked = True - world.push_item(world.get_location('Trench 1 Switch', player), ItemFactory('Trench 1 Filled', player), False) - world.get_location('Trench 1 Switch', player).event = True - world.get_location('Trench 1 Switch', player).locked = True - world.push_item(world.get_location('Trench 2 Switch', player), ItemFactory('Trench 2 Filled', player), False) - world.get_location('Trench 2 Switch', player).event = True - world.get_location('Trench 2 Switch', player).locked = True - world.push_item(world.get_location('Swamp Drain', player), ItemFactory('Drained Swamp', player), False) - world.get_location('Swamp Drain', player).event = True - world.get_location('Swamp Drain', player).locked = True - world.push_item(world.get_location('Turtle Medallion Pad', player), ItemFactory('Turtle Opened', player), False) - world.get_location('Turtle Medallion Pad', player).event = True - world.get_location('Turtle Medallion Pad', player).locked = True - world.push_item(world.get_location('Attic Cracked Floor', player), ItemFactory('Shining Light', player), False) - world.get_location('Attic Cracked Floor', player).event = True - world.get_location('Attic Cracked Floor', player).locked = True - world.push_item(world.get_location('Suspicious Maiden', player), ItemFactory('Maiden Rescued', player), False) - world.get_location('Suspicious Maiden', player).event = True - world.get_location('Suspicious Maiden', player).locked = True - world.push_item(world.get_location('Revealing Light', player), ItemFactory('Maiden Unmasked', player), False) - world.get_location('Revealing Light', player).event = True - world.get_location('Revealing Light', player).locked = True - world.push_item(world.get_location('Ice Block Drop', player), ItemFactory('Convenient Block', player), False) - world.get_location('Ice Block Drop', player).event = True - world.get_location('Ice Block Drop', player).locked = True - world.push_item(world.get_location('Skull Star Tile', player), ItemFactory('Hidden Pits', player), False) - world.get_location('Skull Star Tile', player).event = True - world.get_location('Skull Star Tile', player).locked = True + for loc, item in location_events.items(): + if item: + set_event_item(loc, item) + if world.mode[player] == 'standard': - world.push_item(world.get_location('Zelda Pickup', player), ItemFactory('Zelda Herself', player), False) - world.get_location('Zelda Pickup', player).event = True - world.get_location('Zelda Pickup', player).locked = True - world.push_item(world.get_location('Zelda Drop Off', player), ItemFactory('Zelda Delivered', player), False) - world.get_location('Zelda Drop Off', player).event = True - world.get_location('Zelda Drop Off', player).locked = True + set_event_item('Zelda Pickup', 'Zelda Herself') + set_event_item('Zelda Drop Off', 'Zelda Delivered') # set up item pool skip_pool_adjustments = False @@ -398,7 +343,8 @@ def generate_itempool(world, player): world.treasure_hunt_icon[player] = 'Triforce Piece' world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player - and ((item.smallkey and world.keyshuffle[player] != 'none') + and ((item.prize and world.prizeshuffle[player] == 'wild') + or (item.smallkey and world.keyshuffle[player] != 'none') or (item.bigkey and world.bigkeyshuffle[player]) or (item.map and world.mapshuffle[player]) or (item.compass and world.compassshuffle[player]))]) @@ -509,7 +455,7 @@ def generate_itempool(world, player): world.itempool = [beemizer(item) for item in world.itempool] # increase pool if not enough items - ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) + ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if world.prizeshuffle[player] != 'none' or not x.prize) pool_size = count_player_dungeon_item_pool(world, player) pool_size += sum(1 for x in world.itempool if x.player == player) @@ -540,7 +486,7 @@ fixed_take_anys = [ def set_up_take_anys(world, player, skip_adjustments=False): - if world.mode[player] == 'inverted': + if world.is_dark_chapel_start(player): if 'Dark Sanctuary Hint' in take_any_locations: take_any_locations.remove('Dark Sanctuary Hint') if world.is_tile_swapped(0x29, player): @@ -748,9 +694,12 @@ def create_dynamic_bonkdrop_locations(world, player): def fill_prizes(world, attempts=15): + from Items import prize_item_table all_state = world.get_all_state(keys=True) for player in range(1, world.players + 1): - crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player) + if world.prizeshuffle[player] != 'none': + continue + crystals = ItemFactory(list(prize_item_table.keys()), player) crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player), world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player), world.get_location('Misery Mire - Prize', player)] @@ -765,6 +714,8 @@ def fill_prizes(world, attempts=15): random.shuffle(prizepool) random.shuffle(prize_locs) fill_restrictive(world, all_state, prize_locs, prizepool, single_player_placement=True) + for prize_loc in crystal_locations: + prize_loc.parent_region.dungeon.prize = prize_loc.item except FillError as e: logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1) for location in empty_crystal_locations: @@ -1442,9 +1393,11 @@ def make_customizer_pool(world, player): target_amount = max(amount, len(dungeon.small_keys)) additional_amount = target_amount - len(dungeon.small_keys) dungeon.small_keys.extend([d_item] * additional_amount) - elif item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass'): + elif (item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass') + or item_name.startswith('Crystal') or item_name.endswith('Pendant')): d_item = ItemFactory(item_name, player) - if ((d_item.bigkey and not world.bigkeyshuffle[player]) + if ((d_item.prize and world.prizeshuffle[player] in ['none', 'dungeon']) + or (d_item.bigkey and not world.bigkeyshuffle[player]) or (d_item.compass and not world.compassshuffle[player]) or (d_item.map and not world.mapshuffle[player])): d_name = d_item.dungeon @@ -1589,11 +1542,11 @@ def set_default_triforce(goal, custom_goal, custom_total): def fill_specific_items(world): if world.customizer: + from Items import prize_item_table placements = world.customizer.get_placements() dungeon_pool = get_dungeon_item_pool(world) prize_pool = [] - prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', - 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'} + prize_set = set(prize_item_table.keys()) for p in range(1, world.players + 1): prize_pool.extend(prize_set) if placements: @@ -1653,11 +1606,17 @@ def fill_specific_items(world): item_name = item_parts[0] world.item_pool_config.restricted[(item_name, item_player)] = placement['locations'] elif placement['type'] == 'PreferredLocationGroup': - item = placement['item'] - item_parts = item.split('#') - item_player = player if len(item_parts) < 2 else int(item_parts[1]) - item_name = item_parts[0] - world.item_pool_config.preferred[(item_name, item_player)] = placement['locations'] + items = [] + if 'item' in placement: + items.append(placement['item']) + elif 'items' in placement: + items.extend(placement['items']) + for item in items: + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + world.item_pool_config.preferred[(item_name, item_player)] = placement['locations'] + world.item_pool_config.reserved_locations[player].update(placement['locations']) elif placement['type'] == 'Verification': item = placement['item'] item_parts = item.split('#') @@ -1695,7 +1654,8 @@ def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_ def is_dungeon_item(item, world, player): - return ((item.startswith('Small Key') and world.keyshuffle[player] == 'none') + return (((item.startswith('Crystal') or item.endswith('Pendant')) and world.prizeshuffle[player] in ['none', 'dungeon']) + or (item.startswith('Small Key') and world.keyshuffle[player] == 'none') or (item.startswith('Big Key') and not world.bigkeyshuffle[player]) or (item.startswith('Compass') and not world.compassshuffle[player]) or (item.startswith('Map') and not world.mapshuffle[player])) diff --git a/Items.py b/Items.py index e9a056b5..d383b987 100644 --- a/Items.py +++ b/Items.py @@ -60,19 +60,19 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'A better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'), 'Progressive Glove': (True, False, None, 0x61, 150, 'A way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'), 'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Arrows'), - 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x37, 0x08], 999, None, None, None, None, None, None, None), - 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x39, 0x09], 999, None, None, None, None, None, None, None), - 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x38, 0x0a], 999, None, None, None, None, None, None, None), + 'Green Pendant': (True, False, 'Prize', 0x37, 250, 'A pendant that\nsome old guy\nwill never see', 'and the green pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Blue Pendant': (True, False, 'Prize', 0x39, 150, 'A pendant that you\'ll never get', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Red Pendant': (True, False, 'Prize', 0x38, 150, 'A pendant that you\'ll never get', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), 'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), 'Power Star': (True, False, None, 0x6B, 100, 'A small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), 'Triforce Piece': (True, False, None, 0x6C, 100, 'A small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'), - 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x20, 0x01], 999, None, None, None, None, None, None, None), - 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x20, 0x02], 999, None, None, None, None, None, None, None), - 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x20, 0x03], 999, None, None, None, None, None, None, None), - 'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x20, 0x04], 999, None, None, None, None, None, None, None), - 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x20, 0x05], 999, None, None, None, None, None, None, None), - 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x20, 0x06], 999, None, None, None, None, None, None, None), - 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x20, 0x07], 999, None, None, None, None, None, None, None), + 'Crystal 1': (True, False, 'Prize', 0xB1, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 2': (True, False, 'Prize', 0xB4, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 3': (True, False, 'Prize', 0xB6, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 4': (True, False, 'Prize', 0xB5, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 5': (True, False, 'Prize', 0xB2, 250, 'A crystal that\nunlocks a bomb', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 6': (True, False, 'Prize', 0xB0, 250, 'A crystal that\nunlocks a bomb', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 7': (True, False, 'Prize', 0xB3, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), 'Single Arrow': (False, False, None, 0x43, 3, 'A lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), 'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), @@ -177,6 +177,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Small Heart': (False, False, None, 0x42, 10, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'), 'Apples': (False, False, None, 0xD1, 30, 'Just a few pieces of fruit!', 'and the juicy fruit', 'the fruity kid', 'the fruit stand', 'expired fruit', 'bottle boy has fruit again', 'an apple hoard'), 'Fairy': (False, False, None, 0xD2, 50, 'Just a pixie!', 'and the pixie', 'the pixie kid', 'pixie for sale', 'pixie fungus', 'bottle boy has pixie again', 'a pixie'), + 'Beat Boss': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Beat Agahnim 1': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Beat Agahnim 2': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Get Frog': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), @@ -202,3 +203,16 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Farmable Bombs': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Farmable Rupees': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), } + +prize_item_table = { + 'Green Pendant': [0x04, 0x38, 0x62, 0x00, 0x69, 0x08], + 'Blue Pendant': [0x02, 0x34, 0x60, 0x00, 0x69, 0x09], + 'Red Pendant': [0x01, 0x32, 0x60, 0x00, 0x69, 0x0a], + 'Crystal 1': [0x02, 0x34, 0x64, 0x40, 0x7F, 0x01], + 'Crystal 2': [0x10, 0x34, 0x64, 0x40, 0x79, 0x02], + 'Crystal 3': [0x40, 0x34, 0x64, 0x40, 0x6C, 0x03], + 'Crystal 4': [0x20, 0x34, 0x64, 0x40, 0x6D, 0x04], + 'Crystal 5': [0x04, 0x32, 0x64, 0x40, 0x6E, 0x05], + 'Crystal 6': [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], + 'Crystal 7': [0x08, 0x34, 0x64, 0x40, 0x7C, 0x07] +} diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index a5ca9cad..a451200e 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -3,7 +3,7 @@ import logging from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType -from Regions import dungeon_events +from Regions import location_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1116,7 +1116,7 @@ def location_is_bk_locked(loc, key_logic): # todo: verfiy this code is defunct # def prize_or_event(loc): -# return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] +# return loc.name in location_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] # # # def reserved_location(loc, world, player): @@ -1504,7 +1504,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk) # prize door should not be opened if the boss is reachable - but not reached yet allow_for_prize_lock = (key_layout.prize_can_lock and - not any(x for x in state.found_locations if '- Prize' in x.name)) + not any(x for x in state.found_locations if x.prize)) prize_done = not key_layout.prize_relevant or state.prize_doors_opened or allow_for_prize_lock if smalls_done and bk_done and prize_done: return False @@ -1623,7 +1623,7 @@ def determine_prize_lock(key_layout, world, player): elif len(state.small_doors) > 0: open_a_door(state.small_doors[0].door, state, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) - if any(x for x in state.found_locations if '- Prize' in x.name): + if any(x for x in state.found_locations if x.prize): key_layout.prize_can_lock = True @@ -1776,7 +1776,7 @@ def create_key_counter(state, key_layout, world, player): key_counter.key_only_locations[loc] = None elif loc.forced_item and loc.item.name == key_layout.key_logic.bk_name: key_counter.other_locations[loc] = None - elif loc.name not in dungeon_events: + elif loc.name not in location_events: key_counter.free_locations[loc] = None else: key_counter.other_locations[loc] = None @@ -1785,7 +1785,7 @@ def create_key_counter(state, key_layout, world, player): key_counter.big_key_opened = state.big_key_opened if len(state.prize_door_set) > 0 and state.prize_doors_opened: key_counter.prize_doors_opened = True - if any(x for x in key_counter.important_locations if '- Prize' in x.name): + if any(x for x in key_counter.important_locations if x.prize): key_counter.prize_received = True return key_counter @@ -1805,7 +1805,7 @@ def imp_locations_factory(world, player): def important_location(loc, world, player): - return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player) or (loc.forced_big_key()) + return loc.prize or loc.name in imp_locations_factory(world, player) or (loc.forced_big_key()) def create_odd_key_counter(door, parent_counter, key_layout, world, player): @@ -2135,9 +2135,9 @@ def validate_key_placement(key_layout, world, player): found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: - found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) + found_prize = any(x for x in counter.important_locations if x.prize) if not found_prize and dungeon_table[key_layout.sector.name].prize: - prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) + prize_loc = dungeon_table[key_layout.sector.name].prize.location if key_layout.prize_relevant == 'BigBomb': found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6'] elif key_layout.prize_relevant == 'GT': diff --git a/Main.py b/Main.py index c36bad50..e2fcc7d1 100644 --- a/Main.py +++ b/Main.py @@ -18,7 +18,6 @@ from PotShuffle import shuffle_pots, shuffle_pot_switches from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations from OWEdges import create_owedges from OverworldShuffle import link_overworld, update_world_regions, create_dynamic_exits -from EntranceShuffle import link_entrances from Rom import patch_rom, patch_race_rom, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors from DoorShuffle import link_doors, connect_portal, link_doors_prep @@ -33,7 +32,7 @@ from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridm from Utils import output_path, parse_player_names from source.item.District import init_districts -from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config +from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config, verify_item_pool_config from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings @@ -41,7 +40,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.1.10' +version_number = '1.4.1.12' version_branch = '-u' __version__ = f'{version_number}{version_branch}' @@ -87,7 +86,6 @@ def main(args, seed=None, fish=None): for i in zip(args.logic.values(), args.door_shuffle.values()): if i[0] == 'hybridglitches' and i[1] != 'vanilla': raise RuntimeError(BabelFish().translate("cli","cli","hybridglitches.door.shuffle")) - if seed is None: random.seed(None) world.seed = random.randint(0, 999999999) @@ -160,7 +158,7 @@ def main(args, seed=None, fish=None): world.settings = CustomSettings() world.settings.create_from_world(world, args) - if args.create_spoiler and not args.jsonout: + if world.spoiler_mode != 'none' and not args.jsonout: logger.info(world.fish.translate("cli", "cli", "create.meta")) world.spoiler.meta_to_file(output_path(f'{outfilebase}_Spoiler.txt')) if args.mystery and not (args.suppress_meta or args.create_spoiler): @@ -231,6 +229,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): generate_itempool(world, player) + verify_item_pool_config(world) logger.info(world.fish.translate("cli","cli","calc.access.rules")) for player in range(1, world.players + 1): @@ -365,12 +364,12 @@ def main(args, seed=None, fish=None): if args.mystery and not (args.suppress_meta or args.create_spoiler): world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) - elif args.create_spoiler and not args.jsonout: + elif world.spoiler_mode != 'none' and not args.jsonout: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.create_spoiler and not args.jsonout: + if world.spoiler_mode != 'none' and not args.jsonout: logger.info(world.fish.translate("cli", "cli", "patching.spoiler")) world.spoiler.to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.loglevel == 'debug': + if 'debug' in world.spoiler.settings: world.spoiler.extras(output_path(f'{outfilebase}_Spoiler.txt')) if not args.skip_playthrough: @@ -379,7 +378,7 @@ def main(args, seed=None, fish=None): if args.jsonout: print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()})) - elif args.create_spoiler: + elif world.spoiler_mode != 'none': logger.info(world.fish.translate("cli","cli","patching.spoiler")) if args.jsonout: with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: @@ -394,7 +393,7 @@ def main(args, seed=None, fish=None): logger.info("") logger.info(world.fish.translate("cli","cli","made.rom") % (YES if (args.create_rom) else NO)) logger.info(world.fish.translate("cli","cli","made.playthrough") % (YES if (args.calc_playthrough) else NO)) - logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and args.create_spoiler) else NO)) + logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and world.spoiler_mode != 'none') else NO)) logger.info(world.fish.translate("cli","cli","seed") + ": %s", world.seed) logger.info(world.fish.translate("cli","cli","total.time"), time.perf_counter() - start) @@ -452,7 +451,7 @@ def init_world(args, fish): world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, - args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints) + args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) world.customizer = customized world.boots_hint = args.boots_hint.copy() @@ -461,6 +460,7 @@ def init_world(args, fish): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() + world.prizeshuffle = args.prizeshuffle.copy() world.bombbag = args.bombbag.copy() world.flute_mode = args.flute_mode.copy() world.bow_mode = args.bow_mode.copy() @@ -521,7 +521,9 @@ def set_starting_inventory(world, args): for p, inv_list in world.customizer.get_start_inventory().items(): if inv_list: for inv_item in inv_list: - item = ItemFactory(inv_item.strip(), p) + name = inv_item.strip() + name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)' + item = ItemFactory(name, p) if item: world.push_precollected(item) @@ -530,7 +532,7 @@ def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() @@ -556,6 +558,7 @@ def copy_world(world): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.prizeshuffle = world.prizeshuffle.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() ret.bow_mode = world.bow_mode.copy() @@ -594,6 +597,7 @@ def copy_world(world): ret.standardize_palettes = world.standardize_palettes.copy() ret.owswaps = world.owswaps.copy() ret.owflutespots = world.owflutespots.copy() + ret.allow_flip_sanc = world.allow_flip_sanc.copy() ret.prizes = world.prizes.copy() ret.restrict_boss_items = world.restrict_boss_items.copy() ret.inaccessible_regions = world.inaccessible_regions.copy() @@ -725,7 +729,7 @@ def copy_world_premature(world, player): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() @@ -751,6 +755,7 @@ def copy_world_premature(world, player): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.prizeshuffle = world.prizeshuffle.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() ret.bow_mode = world.bow_mode.copy() @@ -789,6 +794,7 @@ def copy_world_premature(world, player): ret.standardize_palettes = world.standardize_palettes.copy() ret.owswaps = world.owswaps.copy() ret.owflutespots = world.owflutespots.copy() + ret.allow_flip_sanc = world.allow_flip_sanc.copy() ret.prizes = world.prizes.copy() ret.restrict_boss_items = world.restrict_boss_items.copy() ret.inaccessible_regions = world.inaccessible_regions.copy() @@ -888,7 +894,7 @@ def copy_dynamic_regions_and_locations(world, ret): for location in world.dynamic_locations: new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) - new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg) + new_loc = Location(location.player, location.name, location.address, location.prize, location.hint_text, new_reg) new_loc.type = location.type new_reg.locations.append(new_loc) diff --git a/MultiClient.py b/MultiClient.py index 3723599f..d28d401f 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -340,6 +340,16 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), 'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400), 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), 'Ganons Tower - Validation Chest': (0x4d, 0x10)} +location_table_boss = {'Eastern Palace - Prize': 0x2000, + 'Desert Palace - Prize': 0x1000, + 'Tower of Hera - Prize': 0x0020, + 'Palace of Darkness - Prize': 0x0200, + 'Thieves Town - Prize': 0x0010, + 'Skull Woods - Prize': 0x0080, + 'Swamp Palace - Prize': 0x0400, + 'Ice Palace - Prize': 0x0040, + 'Misery Mire - Prize': 0x0100, + 'Turtle Rock - Prize': 0x0008} location_table_npc = {'Mushroom': 0x1000, 'King Zora': 0x2, 'Sahasrahla': 0x10, @@ -949,6 +959,14 @@ async def track_locations(ctx : Context, roomid, roomdata): if roomdata & mask != 0: new_check(location) + if not all([location in ctx.locations_checked for location in location_table_boss.keys()]): + boss_data = await snes_read(ctx, SAVEDATA_START + 0x472, 2) + if boss_data is not None: + boss_value = boss_data[0] | (boss_data[1] << 8) + for location, mask in location_table_boss.items(): + if boss_value & mask != 0 and location not in ctx.locations_checked: + new_check(location) + ow_begin = 0x82 ow_end = 0 ow_unchecked = {} diff --git a/Mystery.py b/Mystery.py index e937ce1b..ca79541d 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,8 +28,9 @@ def main(): parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--names', default='') parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) - parser.add_argument('--create_spoiler', action='store_true') + parser.add_argument('--spoiler', default='none', choices=['none', 'settings', 'semi', 'full', 'debug']) parser.add_argument('--no_race', action='store_true') + parser.add_argument('--print_template_yaml', action='store_true') parser.add_argument('--suppress_rom', action='store_true') parser.add_argument('--suppress_meta', action='store_true') parser.add_argument('--bps', action='store_true') @@ -64,9 +65,10 @@ def main(): erargs = parse_cli(['--multi', str(args.multi)]) erargs.seed = seed erargs.names = args.names - erargs.create_spoiler = args.create_spoiler + erargs.spoiler = args.spoiler erargs.suppress_rom = args.suppress_rom erargs.suppress_meta = args.suppress_meta + erargs.print_template_yaml = args.print_template_yaml erargs.bps = args.bps erargs.race = not args.no_race erargs.outputname = seedname diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6f977c40..8b4f332e 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.4.0.2' +version_number = '0.5.0.0' # branch indicator is intentionally different across branches version_branch = '' @@ -151,7 +151,8 @@ def link_overworld(world, player): # tile shuffle logging.getLogger('').debug('Flipping overworld tiles') if world.owMixed[player]: - tile_groups, force_flipped, force_nonflipped, undefined_chance = define_tile_groups(world, False, player) + tile_groups, force_flipped, force_nonflipped, undefined_chance, allow_flip_sanc = define_tile_groups(world, False, player) + world.allow_flip_sanc[player] = allow_flip_sanc swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], False, (force_flipped, force_nonflipped, undefined_chance), player) update_world_regions(world, player) @@ -180,7 +181,7 @@ def link_overworld(world, player): else: connect_simple(world, 'Links House S&Q', 'Big Bomb Shop', player) - if not world.mode[player] == 'inverted': + if not world.is_dark_chapel_start(player): connect_simple(world, 'Sanctuary S&Q', 'Sanctuary', player) else: connect_simple(world, 'Sanctuary S&Q', 'Dark Sanctuary Hint', player) @@ -241,7 +242,7 @@ def link_overworld(world, player): # the idea is to XOR the new flips with the ones from Mixed so that non-parallel edges still work # Polar corresponds to Grouped with no flips in ow_crossed_tiles_mask ow_crossed_tiles_mask = [[],[],[]] - tile_groups, force_flipped, force_nonflipped, undefined_chance = define_tile_groups(world, True, player) + tile_groups, force_flipped, force_nonflipped, undefined_chance, _ = define_tile_groups(world, True, player) world.owcrossededges[player] = shuffle_tiles(world, tile_groups, ow_crossed_tiles_mask, True, (force_flipped, force_nonflipped, undefined_chance), player) ow_crossed_tiles = [i for i in range(0x82) if (i in world.owswaps[player][0]) != (i in ow_crossed_tiles_mask[0])] @@ -885,6 +886,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): def determine_forced_flips(world, tile_ow_groups, do_grouped, player): undefined_chance = 50 + allow_flip_sanc = do_grouped flipped_groups = list() nonflipped_groups = list() merged_owids = list() @@ -899,6 +901,8 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player): forced_nonflips = list() if 'undefined_chance' in custom_flips: undefined_chance = custom_flips['undefined_chance'] + if not do_grouped and 'always_allow_flipped_sanctuary' in custom_flips: + allow_flip_sanc = custom_flips['always_allow_flipped_sanctuary'] in [1, True, "True", "true"] if 'force_flip' in custom_flips: forced_flips = custom_flips['force_flip'] if 'force_no_flip' in custom_flips: @@ -974,7 +978,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player): # Check if there are any groups that appear in both sets if any(group in flipped_groups for group in nonflipped_groups): raise GenerationException('Conflict found when flipping tiles') - return flipped_groups, nonflipped_groups, undefined_chance, merged_owids + return flipped_groups, nonflipped_groups, undefined_chance, allow_flip_sanc, merged_owids def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): (flipped_groups, nonflipped_groups, undefined_chance) = forced_flips @@ -1045,9 +1049,13 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): # tile shuffle happens here removed = [] - if 0 < undefined_chance < 100: - for group in groups: - if group[0] in nonflipped_groups or (group[0] not in flipped_groups and random.randint(1, 100) > undefined_chance): + for group in groups: + if group[0] in nonflipped_groups: + removed.append(group) + else: + if group[0] in flipped_groups or undefined_chance >= 100: + continue + if undefined_chance == 0 or random.randint(1, 100) > undefined_chance: removed.append(group) # save shuffled tiles to list @@ -1072,7 +1080,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): attempts -= 1 continue # ensure sanc can be placed in LW in certain modes - if not do_grouped and world.shuffle[player] in ['simple', 'restricted', 'full', 'district'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): + if not do_grouped and world.shuffle[player] in ['simple', 'restricted', 'full', 'district'] and not world.is_dark_chapel_start(player) and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): free_dw_drops = parity[5] + (1 if world.shuffle_ganon[player] else 0) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon[player] else 0) if free_dw_drops == free_drops: @@ -1124,9 +1132,9 @@ def define_tile_groups(world, do_grouped, player): return False # sanctuary/chapel should not be flipped if S+Q guaranteed to output on that screen - if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district'] \ - and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ - or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')): + if 0x13 in group and not allow_flip_sanc and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district'] \ + and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] not in ['partitioned', 'crossed'] \ + or world.intensity[player] < 3)) or (world.shuffle[player] in ['lite', 'lean'] and world.is_dark_chapel_start(player))): return False return True @@ -1167,7 +1175,7 @@ def define_tile_groups(world, do_grouped, player): merge_groups([[0x0f, 0x35], [0x12, 0x15, 0x33, 0x3f]]) # customizer adjustments - flipped_groups, nonflipped_groups, undefined_chance, merged_owids = determine_forced_flips(world, groups, do_grouped, player) + flipped_groups, nonflipped_groups, undefined_chance, allow_flip_sanc, merged_owids = determine_forced_flips(world, groups, do_grouped, player) for owids in merged_owids: merge_groups([owids]) @@ -1181,7 +1189,7 @@ def define_tile_groups(world, do_grouped, player): tile_groups.append((group, lw_regions, dw_regions)) random.shuffle(tile_groups) - return tile_groups, flipped_groups, nonflipped_groups, undefined_chance + return tile_groups, flipped_groups, nonflipped_groups, undefined_chance, allow_flip_sanc def remove_reserved(world, groupedlist, connected_edges, player): new_grouping = {} @@ -1367,8 +1375,6 @@ def update_world_regions(world, player): def can_reach_smith(world, player): from Items import ItemFactory from BaseClasses import CollectionState - - invFlag = world.mode[player] == 'inverted' def explore_region(region_name, region=None): nonlocal found @@ -1407,7 +1413,7 @@ def can_reach_smith(world, player): start_region = 'Big Bomb Shop' explore_region(start_region) if not found: - if not invFlag: + if not world.is_dark_chapel_start(player): if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': sanc_mirror = world.get_entrance('Sanctuary Mirror Route', player) explore_region(sanc_mirror.connected_region.name, sanc_mirror.connected_region) @@ -1544,7 +1550,8 @@ def validate_layout(world, player): from Main import copy_world_premature from Utils import stack_size3a - from EntranceShuffle import default_dungeon_connections, default_connector_connections, default_item_connections, default_shop_connections, default_drop_connections, default_dropexit_connections + # TODO: Find a better source for the below lists, original sourced was deprecated + from source.overworld.EntranceData import default_dungeon_connections, default_connector_connections, default_item_connections, default_shop_connections, default_drop_connections, default_dropexit_connections dungeon_entrances = list(zip(*default_dungeon_connections + [('Ganons Tower', '')]))[0] connector_entrances = list(zip(*default_connector_connections))[0] @@ -1586,7 +1593,7 @@ def validate_layout(world, player): start_region = 'Big Bomb Shop Area' explore_region(start_region) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean'] and world.mode[player] == 'inverted': + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean'] and world.is_dark_chapel_start(player): start_region = 'Dark Chapel Area' explore_region(start_region) diff --git a/Plando.py b/Plando.py index 2b11bed7..91780d5d 100755 --- a/Plando.py +++ b/Plando.py @@ -10,7 +10,7 @@ import sys from BaseClasses import World from Regions import create_regions from OverworldShuffle import link_overworld -from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit +from source.overworld.EntranceShuffle2 import link_entrances_new, connect_entrance, connect_two_way, connect_exit from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name from Rules import set_rules from Dungeons import create_dungeons @@ -48,7 +48,7 @@ def main(args): link_overworld(world, 1) - link_entrances(world, 1) + link_entrances_new(world, 1) logger.info('Calculating Access Rules.') diff --git a/README.md b/README.md index afdd4201..12ac61e5 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ For a first (and second) seed... *and I say "second" because I feel like both of - `Bonk Drops` - Recommend to NOT turn this on, especially if you don't have starting boots, but you could enable this in future seeds if you've become more familiar with OWR, as you'll be visiting these screens anyways, you might as well grab the items on the way :) ## "What tracker should I use?" -I personally use 2 trackers together, at least if Layout Shuffle is enabled. Currently, DunkaTracker is the ONLY tracker that is useful for tracking Layout Shuffle, can highly recommend. However, I personally don't like the main portion of the tracker and ignore the main window completely. For tracking everything else, I use `CodeTracker`, a tracker pack that is installable within `EmoTracker`, this handles ALL OWR modes EXCEPT Layout Shuffle (and generally Crossed OW, but missing that doesn't make much of a difference). I am unaware of ANY trackers outside of these 2 that handle OWR modes, so my advice would be to familiarize yourself with both trackers and pick and choose the parts you find most useful for yourself. +I personally use 2 trackers together, at least if Layout Shuffle is enabled. Currently, DunkaTracker is the ONLY tracker that is useful for tracking Layout Shuffle, highly recommended. However, I personally don't like the main portion of the tracker and ignore the main window completely. For tracking everything else, I use `CodeTracker`, a tracker pack that is installable within `EmoTracker`, this handles ALL OWR modes EXCEPT Layout Shuffle (and generally Crossed OW, but missing that doesn't make much of a difference). I am unaware of ANY trackers outside of these 2 that handle OWR modes, so my advice would be to familiarize yourself with both trackers and pick and choose the parts you find most useful for yourself. # Terminology @@ -105,6 +105,10 @@ Similar edges on the larger OW tiles are a bit harder to intuitively determine. Similar edges are used when `Keep Similar Edges Together` is enabled and have meaning when used with `Layout Shuffle` (with and without `Free Terrain`) and `Crossed` settings. +# Overworld Map Changes + +The overworld map check screen has been completely overhauled in this OWR fork. In other LTTP forks, there are numbered icons to indicate the crystal number of the crystal and those are shown alongside crystal prizes. OWR has changed this so the numbers instead correspond to dungeon numbers. In the LW, you have the 3 pendant dungeons, these are indicated by a blue 1, 2, and 3. In the DW, you have the 7 crystal dungeon, these are indicated by red numbers (1-7). In addition, there may be some mode combinations where HC, AT, and GT may be visible via icon, indicated by a white H, a white A, and a skull. An example of what you can expect to see on a map check can be found [here](https://cdn.discordapp.com/attachments/783989090017738753/1243839950663847936/newmapcheck.gif?ex=6652efb9&is=66519e39&hm=8be1d514458c09d6881a0d6ae0e539adf1f6227374a7d61a8f3a1831e18f0395&). + # Inverted Changes The version of Inverted included in OWR varies quite a bit compared to the Door Rando and VT (main rando) forks. This is often referred to as Inverted 2.0, as this is a plan to shift it to a newer/updated concept, intended to either enhance/improve the Inverted experience or to restore some behaviors more closer to vanilla. Some of these changes are likely going to be added to all rando forks/branches and subject to change, but this will be based on user feedback. @@ -122,7 +126,7 @@ The version of Inverted included in OWR varies quite a bit compared to the Door - Ice Palace has been re-sealed to vanilla, portal moved to outer edge of moat (makes IP mirror locked) - Glitched modes will now use vanilla terrain except where necessary -Note: These changes do impact the logic. If you use `CodeTracker`, these Inverted 2.0 logic rules are automatically detected if using autotracker, indicated by a 2 in the corner of the World State mode icon. This can also be manually applied if you right-click the World State mode icon. +Note: These changes do impact the logic. If you use `CodeTracker`, these Inverted 2.0 logic rules are automatically detected if using autotracker, indicated by a 2 in the corner of the World State mode icon. This can also be manually applied if you right-click the World State mode icon. # Settings @@ -189,7 +193,7 @@ Being that this uses concepts from Inverted, it will be important to review the During gameplay: - When on the OW, there will be an L or D in the upper left corner, indicating which world you are currently in. Mirroring still works the same, you must be in the DW to mirror to the LW. - - When doing a map check (pressing X while on the OW), the tiles shown will reflect the flipped tiles. This means that dungeon prizes will show the prizes for the dungeons that are now part of that world, beware of Desert/Mire and Eastern/PoD. Here is an image showing the difference of appearance when tiles are flipped on the [map check](https://cdn.discordapp.com/attachments/783989090017738753/970646558049714196/lttp-lw-mapcheck.gif?ex=66015e8d&is=65eee98d&hm=b4d97c52d6aed593f0e6ec54924696ba969ce11109ce5ba1291b50a8a3e2dac8&) screen. + - When doing a map check (pressing X while on the OW), the tiles shown will reflect the flipped tiles. This means that dungeon prizes will show the prizes for the dungeons that are now part of that world. Here is an image showing the difference of appearance when tiles are flipped on the [map check](https://cdn.discordapp.com/attachments/783989090017738753/970646558049714196/lttp-lw-mapcheck.gif?ex=665272cd&is=6651214d&hm=6962fe0b16a7919a91066e96fea29d28fbecd404e6c0dc344146f17553425296&) screen. Note: Tiles are put into Tile Groups (see `Terminology`) that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to a different tile, then those tiles must flip together. @@ -248,7 +252,7 @@ This adds 42 new item locations to the game. These bonk locations are limited to - Some screens are coded to change the "alternate tree color", some of them are strange (just how the vanilla game does it) - Rocks and statues are unable to be made to have a different color -Here is a map that shows all the [Bonk Locations](https://cdn.discordapp.com/attachments/1105770688649895968/1105770806769877072/bonkdrops.png?ex=6603d650&is=65f16150&hm=3576367abd636ba7723ef30e87a4bc407c5e1eb9a8be325e90b1e22c04c58401&). FYI, the numbers indicate how many bonk items there. The stars with a green square are all Bonk Locations that are unlocked after you kill Aga 1. +Here is a map that shows all the [Bonk Locations](https://cdn.discordapp.com/attachments/1105770688649895968/1105770806769877072/bonkdrops.png?ex=66524790&is=6650f610&hm=7c16f009d514256d3fdd02667f9876fae4178ec2989e5f1e0cfd32c7207e144e&). FYI, the numbers indicate how many bonk items there. The stars with a green square are all Bonk Locations that are unlocked after you kill Aga 1. As far as map trackers, Bonk Locations are supported on `CodeTracker` when the Bonk Drops option is enabled. @@ -264,6 +268,24 @@ As far as map trackers, Bonk Locations are supported on `CodeTracker` when the B - 1 8x Bomb Pack - 1 Good Bee +## Prize Shuffle + +A new option has been added to shuffle the 10 dungeon prizes in ways that they haven't been shuffled before. This means that dungeon prizes can be found in other item locations, such as chests or free-standing item locations. This also means that bosses are able to drop a 2nd item in place of the shuffled prize. + +### On Boss + +This is the normal prize behavior that has been a part of rando up until now. The dungeon prizes are shuffled amongst themselves in their own pool and aren't considered part of the main collection total. + +### In Dungeon + +This option shuffles the prize into a location somewhere within the dungeon that it is assigned to. + +### Randomized + +This option freely shuffles the prizes throughout the world. While the dungeon prizes can end up anywhere, they still are assigned to a specific dungeon. When you defeat the boss of a certain dungeon, checking the map on the overworld will reveal the location WHERE you can find the prize, an example shown [here](https://cdn.discordapp.com/attachments/783989090017738753/1243840288867487754/prizemap-all.gif?ex=6652f00a&is=66519e8a&hm=a49ef2c84d9862349e331ae87c99c3ce34ae5cd0690073521423d7e61c5429dc&). Finding the map will still reveal WHAT the prize is. If you defeated a boss but haven't collected the map for that dungeon, the prize will be indicated by a red X, example shown [here](https://cdn.discordapp.com/attachments/783989090017738753/1243840289278263316/prizemap-boss.gif?ex=6652f00a&is=66519e8a&hm=f1d75388b2ca039f5c35f244109ff659b34235d6ce2f76516ad46f978ec49f91&). If you collected a map but haven't defeated the boss yet, the icon indicator on the map will be shown on the top edge (for LW dungeons) or the bottom edge (for DW dungeons), but it will show you WHAT the prize is for that dungeon, an example of that is shown [here](https://cdn.discordapp.com/attachments/783989090017738753/1243840289718669372/prizemap-map.gif?ex=6652f00a&is=66519e8a&hm=47fa004f493c63842bec3a54b7703d95d9a6a05067fa3fb539d48de9c0cb1698&). + +- It is important to note that the overworld map check has changed: the numbered icons that are displayed are NO LONGER indicating the crystal number like they have in the past. They are now indicating the dungeon that it belongs to; a blue 1-3 indicates the 3 LW dungeons (EP, DP, and ToH) and a red 1-7 indicate the 7 DW dungeons + ## New Goal Options (--goal) ### Trinity @@ -327,7 +349,7 @@ Here is a [Swapped ER Reference Sheet](http://zelda.codemann8.com/images/shared/ This is an entrance shuffle that only shuffles entrances within their respective `Districts` (See Below). Also, dropdowns and the entrance that a dropdown normally exits are NOT kept together; this also means that the Skull Woods entrances are no longer guaranteed to lead to Skull Woods. Also, since there is no district that can span multiple worlds, this is NOT a cross-world entrance mode. -Districts are a concept originally conceived by Aerinon in the Door Randomizer, where parts of the OW are split into areas and given a name. Here is a [District Map](https://cdn.discordapp.com/attachments/783989090017738753/1194615705027477534/districts.png?ex=66040e12&is=65f19912&hm=9ef382f004f7013e018f0b04d0bc98727f87b8b5da6499f34dbcf6e14bb0ac90&) showing how they are split up. +Districts are a concept originally conceived by Aerinon in the Door Randomizer, where parts of the OW are split into areas and given a name. Here is a [District Map](https://cdn.discordapp.com/attachments/783989090017738753/1194615705027477534/districts.png?ex=66527f52&is=66512dd2&hm=6f29d8df27eea6a3489b3bb74381e02fb9706158c2f00bcef652f156a53c1268&) showing how they are split up. # Command Line Options diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8d57d657..a840e5e4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -141,6 +141,30 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes +* 1.4.1.12u + * New Entrance Shuffle Algorithm no longer experimental + * Back of Tavern Shuffle now on by default + * Enemizer: Wallmasters banned from tiles where spiral staircases are. (Softlock issue) + * Packaged build of unstable now available + * Customizer: New PreferredLocationGroup for putting a set of items in a set of locations. See customizer docs. + * Customizer: Fixed an issue with starting with `Ocarina` and flute_mode is active + * Spoiler: Some reformatting. Crystal req. for GT/Ganon moved to requirements section so randomized requirements don't show up in the meta section + * Algorithm: Major_Only. Supports up to 16 extra locations (the visible heart pieces) for when major item count exceeds major location count. Examples: Triforce Hunt, Trinity (Triforce on Ped), Bombbag shuffle + * Fix: HC Big Key drop doesn't count on Basic Doors + * Fix: Small Key for this dungeon in Hera Basement doesn't count twice for the key counter + * Fix: All cross-dungeon modes with restrict boss items should require map/compass for the boss + * Fixed a small bug with traversal algorithm + * Enemizer: Enemy bans+ +* 1.4.1.11u + * New Feature: Several spoiler levels added: None, Settings-only, Semi, Full, Debug + * Semi includes only entrances, prizes, and medallions (potential new spoiler mode being worked on, definition may change) + * Entrance: Lite/Lean support enemy drop shuffle + * Standard: Re-added tutorial guard near large rock + * Enemizer + * Fixed the overwriting of bonk fairies + * Fixed broken graphics on hyrule castle + * Enemy bans + * Customizer: Fixed bug with customizing prize packs * 1.4.1.10u * Vanilla key logic: Fix for vanilla layout Misery Mire which allows more complex key logic. Locations blocked by crystal switch access are only locked by 2 keys thanks to that being the minimum in Mire to reach one of two crystal switches. * Autotracking: Fix for chest turn counter with chest containing multiworld items (Thanks Hiimcody) diff --git a/Regions.py b/Regions.py index db9ad299..5d01b959 100644 --- a/Regions.py +++ b/Regions.py @@ -276,7 +276,8 @@ def create_regions(world, player): 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump', 'Paradox Cave Chest Area NE']), create_cave_region(player, 'Paradox Cave Bomb Area', 'a connector', ['Paradox Cave Upper - Left', 'Paradox Cave Upper - Right']), - create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), + create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Climb', 'Paradox Cave Drop']), + create_cave_region(player, 'Paradox Cave (Top)', 'a connector', None, ['Paradox Cave Exit (Top)', 'Paradox Cave Descent']), create_cave_region(player, 'Paradox Shop', 'a common shop', ['Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right']), create_cave_region(player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), create_cave_region(player, 'Fortune Teller (Light)', 'a fortune teller'), @@ -383,7 +384,6 @@ def create_regions(world, player): def create_dungeon_regions(world, player): std_flag = world.mode[player] == 'standard' - inv_flag = world.mode[player] == 'inverted' world.regions += [ create_dungeon_region(player, 'Sanctuary Portal', 'Hyrule Castle', None, ['Sanctuary Exit', 'Enter HC (Sanc)']), create_dungeon_region(player, 'Hyrule Castle West Portal', 'Hyrule Castle', None, ['Hyrule Castle Exit (West)', 'Enter HC (West)']), @@ -489,7 +489,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Eastern Cannonball Hell', 'Eastern Palace', None, ['Eastern Cannonball Hell ES', 'Eastern Cannonball Hell WS']), create_dungeon_region(player, 'Eastern Single Eyegore', 'Eastern Palace', None, ['Eastern Single Eyegore ES', 'Eastern Single Eyegore NE']), create_dungeon_region(player, 'Eastern Duo Eyegores', 'Eastern Palace', None, ['Eastern Duo Eyegores SE', 'Eastern Duo Eyegores NE']), - create_dungeon_region(player, 'Eastern Boss', 'Eastern Palace', ['Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Boss SE']), + create_dungeon_region(player, 'Eastern Boss', 'Eastern Palace', None, ['Eastern Boss SE', 'Eastern Palace Boss']), + create_dungeon_region(player, 'Eastern Boss Spoils', 'Eastern Palace', ['Eastern Palace - Boss', 'Eastern Palace - Prize', 'Eastern Palace - Boss Kill']), # Desert Palace create_dungeon_region(player, 'Desert Main Lobby', 'Desert Palace', None, ['Desert Main Lobby S', 'Desert Main Lobby N Edge', 'Desert Main Lobby Left Path', 'Desert Main Lobby Right Path']), @@ -518,7 +519,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Desert Beamos Hall', 'Desert Palace', ['Desert Palace - Beamos Hall Pot Key'], ['Desert Beamos Hall WS', 'Desert Beamos Hall NE']), create_dungeon_region(player, 'Desert Tiles 2', 'Desert Palace', ['Desert Palace - Desert Tiles 2 Pot Key'], ['Desert Tiles 2 SE', 'Desert Tiles 2 NE']), create_dungeon_region(player, 'Desert Wall Slide', 'Desert Palace', None, ['Desert Wall Slide SE', 'Desert Wall Slide NW']), - create_dungeon_region(player, 'Desert Boss', 'Desert Palace', ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Boss SW']), + create_dungeon_region(player, 'Desert Boss', 'Desert Palace', None, ['Desert Boss SW', 'Desert Palace Boss']), + create_dungeon_region(player, 'Desert Boss Spoils', 'Desert Palace', ['Desert Palace - Boss', 'Desert Palace - Prize', 'Desert Palace - Boss Kill']), # Hera create_dungeon_region(player, 'Hera Lobby', 'Tower of Hera', None, ['Hera Lobby S', 'Hera Lobby to Crystal', 'Hera Lobby to Front Barrier - Blue']), @@ -546,7 +548,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Hera 5F', 'Tower of Hera', None, ['Hera 5F Down Stairs', 'Hera 5F Up Stairs', 'Hera 5F Star Hole', 'Hera 5F Pothole Chain', 'Hera 5F Normal Holes', 'Hera 5F Orange Path']), create_dungeon_region(player, 'Hera 5F Pot Block', 'Tower of Hera', None), create_dungeon_region(player, 'Hera Fairies', 'Tower of Hera', None, ['Hera Fairies\' Warp']), - create_dungeon_region(player, 'Hera Boss', 'Tower of Hera', ['Tower of Hera - Boss', 'Tower of Hera - Prize'], ['Hera Boss Down Stairs', 'Hera Boss Outer Hole', 'Hera Boss Inner Hole']), + create_dungeon_region(player, 'Hera Boss', 'Tower of Hera', None, ['Hera Boss Down Stairs', 'Hera Boss Outer Hole', 'Hera Boss Inner Hole', 'Tower of Hera Boss']), + create_dungeon_region(player, 'Hera Boss Spoils', 'Tower of Hera', ['Tower of Hera - Boss', 'Tower of Hera - Prize', 'Tower of Hera - Boss Kill']), # AgaTower create_dungeon_region(player, 'Tower Lobby', 'Castle Tower', None, ['Tower Lobby NW', 'Tower Lobby S']), @@ -621,7 +624,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'PoD Turtle Party', 'Palace of Darkness', None, ['PoD Turtle Party ES', 'PoD Turtle Party NW']), create_dungeon_region(player, 'PoD Dark Alley', 'Palace of Darkness', None, ['PoD Dark Alley NE']), create_dungeon_region(player, 'PoD Callback', 'Palace of Darkness', None, ['PoD Callback WS', 'PoD Callback Warp']), - create_dungeon_region(player, 'PoD Boss', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize'], ['PoD Boss SE']), + create_dungeon_region(player, 'PoD Boss', 'Palace of Darkness', None, ['PoD Boss SE', 'Palace of Darkness Boss']), + create_dungeon_region(player, 'PoD Boss Spoils', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize', 'Palace of Darkness - Boss Kill']), # swamp create_dungeon_region(player, 'Swamp Lobby', 'Swamp Palace', None, ['Swamp Lobby S', 'Swamp Lobby Moat']), @@ -674,7 +678,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Swamp Waterway', 'Swamp Palace', ['Swamp Palace - Waterway Pot Key'], ['Swamp Waterway NE', 'Swamp Waterway N', 'Swamp Waterway NW']), create_dungeon_region(player, 'Swamp I', 'Swamp Palace', None, ['Swamp I S']), create_dungeon_region(player, 'Swamp T', 'Swamp Palace', None, ['Swamp T SW', 'Swamp T NW']), - create_dungeon_region(player, 'Swamp Boss', 'Swamp Palace', ['Swamp Palace - Boss', 'Swamp Palace - Prize'], ['Swamp Boss SW']), + create_dungeon_region(player, 'Swamp Boss', 'Swamp Palace', None, ['Swamp Boss SW', 'Swamp Palace Boss']), + create_dungeon_region(player, 'Swamp Boss Spoils', 'Swamp Palace', ['Swamp Palace - Boss', 'Swamp Palace - Prize', 'Swamp Palace - Boss Kill']), # sw create_dungeon_region(player, 'Skull 1 Lobby', 'Skull Woods', None, ['Skull 1 Lobby S', 'Skull 1 Lobby WS', 'Skull 1 Lobby ES']), @@ -702,7 +707,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Skull Vines', 'Skull Woods', None, ['Skull Vines EN', 'Skull Vines NW']), create_dungeon_region(player, 'Skull Spike Corner', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop'], ['Skull Spike Corner SW', 'Skull Spike Corner ES']), create_dungeon_region(player, 'Skull Final Drop', 'Skull Woods', None, ['Skull Final Drop WS', 'Skull Final Drop Hole']), - create_dungeon_region(player, 'Skull Boss', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), + create_dungeon_region(player, 'Skull Boss', 'Skull Woods', None, ['Skull Woods Boss']), + create_dungeon_region(player, 'Skull Boss Spoils', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize', 'Skull Woods - Boss Kill']), # tt create_dungeon_region(player, 'Thieves Lobby', 'Thieves\' Town', ['Thieves\' Town - Map Chest'], ['Thieves Lobby S', 'Thieves Lobby N Edge', 'Thieves Lobby NE Edge', 'Thieves Lobby E']), @@ -712,9 +718,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Thieves Compass Room', 'Thieves\' Town', ['Thieves\' Town - Compass Chest'], ['Thieves Compass Room NW Edge', 'Thieves Compass Room N Edge', 'Thieves Compass Room WS Edge', 'Thieves Compass Room W']), create_dungeon_region(player, 'Thieves Big Chest Nook', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest'], ['Thieves Big Chest Nook ES Edge']), create_dungeon_region(player, 'Thieves Hallway', 'Thieves\' Town', ['Thieves\' Town - Hallway Pot Key'], ['Thieves Hallway SE', 'Thieves Hallway NE', 'Thieves Hallway WN', 'Thieves Hallway WS']), - create_dungeon_region(player, 'Thieves Boss', 'Thieves\' Town', ['Revealing Light', 'Thieves\' Town - Boss', 'Thieves\' Town - Prize'], ['Thieves Boss SE']), - #create_dungeon_region(player, 'Thieves Boss', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize'], ['Revealing Light', 'Thieves Boss SE']), - #create_dungeon_region(player, 'Thieves Revealing Light', 'Thieves\' Town', ['Revealing Light'], ['Thieves Boss Room']), + create_dungeon_region(player, 'Thieves Boss', 'Thieves\' Town', ['Revealing Light'], ['Thieves Boss SE', 'Thieves Town Boss']), + create_dungeon_region(player, 'Thieves Boss Spoils', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize', 'Thieves\' Town - Boss Kill']), create_dungeon_region(player, 'Thieves Pot Alcove Mid', 'Thieves\' Town', None, ['Thieves Pot Alcove Mid ES', 'Thieves Pot Alcove Mid WS']), create_dungeon_region(player, 'Thieves Pot Alcove Bottom', 'Thieves\' Town', None, ['Thieves Pot Alcove Bottom SW']), create_dungeon_region(player, 'Thieves Pot Alcove Top', 'Thieves\' Town', None, ['Thieves Pot Alcove Top NW']), @@ -791,7 +796,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Ice Refill - Crystal', 'Ice Palace', None, ['Ice Refill Crystal Exit']), create_dungeon_region(player, 'Ice Fairy', 'Ice Palace', None, ['Ice Fairy Warp']), create_dungeon_region(player, 'Ice Antechamber', 'Ice Palace', None, ['Ice Antechamber NE', 'Ice Antechamber Hole']), - create_dungeon_region(player, 'Ice Boss', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), + create_dungeon_region(player, 'Ice Boss', 'Ice Palace', None, ['Ice Palace Boss']), + create_dungeon_region(player, 'Ice Boss Spoils', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize', 'Ice Palace - Boss Kill']), # mire create_dungeon_region(player, 'Mire Lobby', 'Misery Mire', None, ['Mire Lobby S', 'Mire Lobby Gap']), @@ -855,7 +861,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Mire Falling Foes', 'Misery Mire', None, ['Mire Falling Foes ES', 'Mire Falling Foes Up Stairs']), create_dungeon_region(player, 'Mire Firesnake Skip', 'Misery Mire', None, ['Mire Firesnake Skip Down Stairs', 'Mire Firesnake Skip Orange Barrier']), create_dungeon_region(player, 'Mire Antechamber', 'Misery Mire', None, ['Mire Antechamber Orange Barrier', 'Mire Antechamber NW']), - create_dungeon_region(player, 'Mire Boss', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize'], ['Mire Boss SW']), + create_dungeon_region(player, 'Mire Boss', 'Misery Mire', None, ['Mire Boss SW', 'Misery Mire Boss']), + create_dungeon_region(player, 'Mire Boss Spoils', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize', 'Misery Mire - Boss Kill']), # tr create_dungeon_region(player, 'TR Main Lobby', 'Turtle Rock', None, ['TR Main Lobby Gap', 'TR Main Lobby SE']), @@ -912,7 +919,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'TR Crystal Maze End - Ranged Crystal', 'Turtle Rock', None, ['TR Crystal Maze End Ranged Crystal Exit']), create_dungeon_region(player, 'TR Final Abyss Balcony', 'Turtle Rock', None, ['TR Final Abyss South Stairs', 'TR Final Abyss Balcony Path']), create_dungeon_region(player, 'TR Final Abyss Ledge', 'Turtle Rock', None, ['TR Final Abyss NW', 'TR Final Abyss Ledge Path']), - create_dungeon_region(player, 'TR Boss', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize'], ['TR Boss SW']), + create_dungeon_region(player, 'TR Boss', 'Turtle Rock', None, ['TR Boss SW', 'Turtle Rock Boss']), + create_dungeon_region(player, 'TR Boss Spoils', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize', 'Turtle Rock - Boss Kill']), # gt create_dungeon_region(player, 'GT Lobby', 'Ganon\'s Tower', None, ['GT Lobby Left Down Stairs', 'GT Lobby Up Stairs', 'GT Lobby Right Down Stairs', 'GT Lobby S']), @@ -1115,8 +1123,8 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None ko_hint = key_drop_data[location][2] ret.locations.append(Location(player, location, None, False, ko_hint, ret, key_drop_data[location][3])) else: - address, player_address, crystal, hint_text = location_table[location] - ret.locations.append(Location(player, location, address, crystal, hint_text, ret, None, player_address)) + address, player_address, prize, hint_text = location_table[location] + ret.locations.append(Location(player, location, address, prize, hint_text, ret, None, player_address)) return ret def mark_light_dark_world_regions(world, player): @@ -1153,8 +1161,8 @@ def create_shops(world, player): world.shops[player] = [] for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram) in shop_table.items(): if world.mode[player] == 'inverted': - if (0x35 not in world.owswaps[player][0] and region_name == 'Dark Lake Hylia Shop') \ - or (0x35 in world.owswaps[player][0] and region_name == 'Lake Hylia Shop'): + if (not world.is_tile_swapped(0x35, player) and region_name == 'Dark Lake Hylia Shop') \ + or (not world.is_tile_swapped(0x35, player) and region_name == 'Lake Hylia Shop'): locked = True inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] custom = True @@ -1231,11 +1239,14 @@ def adjust_locations(world, player): # player address? it is in the shop table index += 1 setup_enemy_locations(world, player) + # disable forced prize locations + if world.prizeshuffle[player] != 'none': + for l in [name for name, data in location_table.items() if data[2]]: + location = world.get_location_unsafe(l, player) + if location: + location.prize = False # unreal events: - for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Frog', 'Missing Smith', 'Dark Blacksmith Ruins', 'Middle Aged Man', - 'Floodgate', 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Turtle Medallion Pad', - 'Attic Cracked Floor', 'Suspicious Maiden', 'Revealing Light', 'Big Bomb', 'Pyramid Crack', - 'Ice Block Drop', 'Lost Old Man', 'Old Man Drop Off', 'Zelda Pickup', 'Zelda Drop Off', 'Skull Star Tile']: + for l in ['Ganon', 'Zelda Pickup', 'Zelda Drop Off'] + list(location_events): location = world.get_location_unsafe(l, player) if location: location.type = LocationType.Logical @@ -1385,18 +1396,42 @@ shop_table_by_location_id = {0x400000+cnt: x for cnt, x in enumerate(flat_normal shop_table_by_location_id = {**shop_table_by_location_id, **{0x400020+cnt: x for cnt, x in enumerate(flat_retro_shops)}} shop_table_by_location = {y: x for x, y in shop_table_by_location_id.items()} -dungeon_events = [ - 'Trench 1 Switch', - 'Trench 2 Switch', - 'Swamp Drain', - 'Attic Cracked Floor', - 'Suspicious Maiden', - 'Revealing Light', - 'Ice Block Drop', - 'Skull Star Tile', - 'Zelda Pickup', - 'Zelda Drop Off' -] + +location_events = { + 'Agahnim 1': 'Beat Agahnim 1', + 'Agahnim 2': 'Beat Agahnim 2', + 'Eastern Palace - Boss Kill': 'Beat Boss', + 'Desert Palace - Boss Kill': 'Beat Boss', + 'Tower of Hera - Boss Kill': 'Beat Boss', + 'Palace of Darkness - Boss Kill': 'Beat Boss', + 'Swamp Palace - Boss Kill': 'Beat Boss', + 'Skull Woods - Boss Kill': 'Beat Boss', + 'Thieves\' Town - Boss Kill': 'Beat Boss', + 'Ice Palace - Boss Kill': 'Beat Boss', + 'Misery Mire - Boss Kill': 'Beat Boss', + 'Turtle Rock - Boss Kill': 'Beat Boss', + 'Lost Old Man': 'Escort Old Man', + 'Old Man Drop Off': 'Return Old Man', + 'Floodgate': 'Open Floodgate', + 'Big Bomb': 'Pick Up Big Bomb', + 'Pyramid Crack': 'Detonate Big Bomb', + 'Frog': 'Get Frog', + 'Missing Smith': 'Return Smith', + 'Dark Blacksmith Ruins': 'Pick Up Purple Chest', + 'Middle Aged Man': 'Deliver Purple Chest', + 'Trench 1 Switch': 'Trench 1 Filled', + 'Trench 2 Switch': 'Trench 2 Filled', + 'Swamp Drain': 'Drained Swamp', + 'Turtle Medallion Pad': 'Turtle Opened', + 'Attic Cracked Floor': 'Shining Light', + 'Suspicious Maiden': 'Maiden Rescued', + 'Revealing Light': 'Maiden Unmasked', + 'Ice Block Drop': 'Convenient Block', + 'Skull Star Tile': 'Hidden Pits', + 'Zelda Pickup': None, + 'Zelda Drop Off': None +} + flooded_keys_reverse = { 'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch', @@ -1512,7 +1547,7 @@ location_table = {'Mushroom': (0x180013, 0x186df8, False, 'in the woods'), 'Pyramid Fairy - Right': (0xe983, 0x186c17, False, 'near a fairy'), 'Brewery': (0xe9ec, 0x186c80, False, 'alone in a home'), 'C-Shaped House': (0xe9ef, 0x186c83, False, 'alone in a home'), - 'Chest Game': (0xeda8, 0x186e2b, False, 'as a prize'), + 'Chest Game': (0xeda8, 0x186e2b, False, 'as a game reward'), 'Bumper Cave Ledge': (0x180146, 0x186e15, False, 'on a ledge'), 'Mire Shed - Left': (0xea73, 0x186d07, False, 'near sparks'), 'Mire Shed - Right': (0xea76, 0x186d0a, False, 'near sparks'), @@ -1623,6 +1658,16 @@ location_table = {'Mushroom': (0x180013, 0x186df8, False, 'in the woods'), 'Ganon': (None, None, False, 'from me'), 'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'), 'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'), + 'Eastern Palace - Boss Kill': (None, None, False, None), + 'Desert Palace - Boss Kill': (None, None, False, None), + 'Tower of Hera - Boss Kill': (None, None, False, None), + 'Palace of Darkness - Boss Kill': (None, None, False, None), + 'Swamp Palace - Boss Kill': (None, None, False, None), + 'Thieves\' Town - Boss Kill': (None, None, False, None), + 'Skull Woods - Boss Kill': (None, None, False, None), + 'Ice Palace - Boss Kill': (None, None, False, None), + 'Misery Mire - Boss Kill': (None, None, False, None), + 'Turtle Rock - Boss Kill': (None, None, False, None), 'Lost Old Man': (None, None, False, None), 'Old Man Drop Off': (None, None, False, None), 'Floodgate': (None, None, False, None), @@ -1643,16 +1688,16 @@ location_table = {'Mushroom': (0x180013, 0x186df8, False, 'in the woods'), 'Skull Star Tile': (None, None, False, None), 'Zelda Pickup': (None, None, False, None), 'Zelda Drop Off': (None, None, False, None), - 'Eastern Palace - Prize': ([0x1209D, 0x53E76, 0x53E77, 0x180052, 0x180070, 0xC6FE, 0x186FE2], None, True, 'Eastern Palace'), - 'Desert Palace - Prize': ([0x1209E, 0x53E7A, 0x53E7B, 0x180053, 0x180072, 0xC6FF, 0x186FE3], None, True, 'Desert Palace'), - 'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706, 0x186FEA], None, True, 'Tower of Hera'), - 'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702, 0x186FE6], None, True, 'Palace of Darkness'), - 'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701, 0x186FE5], None, True, 'Swamp Palace'), - 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707, 0x186FEB], None, True, 'Thieves Town'), - 'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704, 0x186FE8], None, True, 'Skull Woods'), - 'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705, 0x186FE9], None, True, 'Ice Palace'), - 'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703, 0x186FE7], None, True, 'Misery Mire'), - 'Turtle Rock - Prize': ([0x120A7, 0x53E80, 0x53E81, 0x18005C, 0x180075, 0xC708, 0x186FEC], None, True, 'Turtle Rock'), + 'Eastern Palace - Prize': (0xC6FE, 0x186E2C, True, 'with the Armos'), + 'Desert Palace - Prize': (0xC6FF, 0x186E2D, True, 'with Lanmolas'), + 'Tower of Hera - Prize': (0xC706, 0x186E2E, True, 'with Moldorm'), + 'Palace of Darkness - Prize': (0xC702, 0x186E2F, True, 'with Helmasaur King'), + 'Swamp Palace - Prize': (0xC701, 0x186E30, True, 'with Arrghus'), + 'Skull Woods - Prize': (0xC704, 0x186E31, True, 'with Mothula'), + 'Thieves\' Town - Prize': (0xC707, 0x186E32, True, 'with Blind'), + 'Ice Palace - Prize': (0xC705, 0x186E33, True, 'with Kholdstare'), + 'Misery Mire - Prize': (0xC703, 0x186E34, True, 'with Vitreous'), + 'Turtle Rock - Prize': (0xC708, 0x186E35, True, 'with Trinexx'), 'Kakariko Shop - Left': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Middle': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Right': (None, None, False, 'for sale in Kakariko'), diff --git a/Rom.py b/Rom.py index b09cb03d..e07e3371 100644 --- a/Rom.py +++ b/Rom.py @@ -10,7 +10,6 @@ import Items import RaceRandom as random import struct import sys -import subprocess try: import bps.apply import bps.io @@ -28,9 +27,10 @@ from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts from Text import LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts from Text import Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc -from Items import ItemFactory -from EntranceShuffle import door_addresses, exit_ids, ow_prize_table +from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc +from Items import ItemFactory, prize_item_table +from source.overworld.EntranceData import door_addresses, ow_prize_table +from source.overworld.EntranceShuffle2 import exit_ids from OverworldShuffle import default_flute_connections, flute_data from InitialSram import InitialSram @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '3147681c6f9f4f84bcf6d9ba1e06a85b' +RANDOMIZERBASEHASH = '096e6adebfa630e827c662f12f79b4cd' class JsonRom(object): @@ -462,32 +462,31 @@ def patch_rom(world, rom, player, team, is_mystery=False): if location.address is None or (type(location.address) is int and location.address >= 0x400000): continue - if not location.crystal: - if location.item is not None: - # Keys in their native dungeon should use the original item code for keys - itemid = handle_native_dungeon(location, itemid) - if world.remote_items[player]: - itemid = list(location_table.keys()).index(location.name) + 1 - assert itemid < 0x100 - rom.write_byte(location.player_address, 0xFF) - elif location.item.player != player: - if location.player_address is not None: - rom.write_byte(location.player_address, location.item.player) - else: - itemid = 0x5A - rom.write_byte(location.address, itemid) - else: - # crystals - for address, value in zip(location.address, itemid): + if location.item is not None: + # Keys in their native dungeon should use the original item code for keys + itemid = handle_native_dungeon(location, itemid) + if world.remote_items[player]: + itemid = list(location_table.keys()).index(location.name) + 1 + assert itemid < 0x100 + rom.write_byte(location.player_address, 0xFF) + elif location.item.player != player: + if location.player_address is not None: + rom.write_byte(location.player_address, location.item.player) + else: + itemid = 0x5A + rom.write_byte(location.address, itemid) + for dungeon in [d for d in world.dungeons if d.player == player]: + if dungeon.prize: + # prizes + for address, value in zip(dungeon_table[dungeon.name].prize, prize_item_table[dungeon.prize.name]): rom.write_byte(address, value) # patch music - music_addresses = dungeon_music_addresses[location.name] if world.mapshuffle[player]: music = random.choice([0x11, 0x16]) else: - music = 0x11 if 'Pendant' in location.item.name else 0x16 - for music_address in music_addresses: + music = 0x11 if 'Pendant' in dungeon.prize.name else 0x16 + for music_address in dungeon_music_addresses[dungeon.name]: rom.write_byte(music_address, music) if world.mapshuffle[player]: @@ -685,8 +684,6 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(0xDBB73 + exit.addresses, exit.target) if exit.name == 'Tavern North': rom.write_byte(0x157D0, exit.target) - if world.mode[player] == 'inverted': - patch_shuffled_dark_sanc(world, rom, player) # setup dr option flags based on experimental, etc. dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal @@ -726,12 +723,13 @@ def patch_rom(world, rom, player, team, is_mystery=False): or (l.type == LocationType.Drop and not l.forced_item) or (l.type == LocationType.Normal and not l.forced_item) or (l.type == LocationType.Bonk and not l.forced_item) - or (l.type == LocationType.Shop and world.shopsanity[player]))] + or (l.type == LocationType.Shop and world.shopsanity[player]) + or (l.type == LocationType.Prize and not l.prize))] valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) # fix hc big key problems (map and compass too) - if (world.doorShuffle[player] not in ['vanilla', 'basic'] or world.dropshuffle[player] != 'none' - or world.pottery[player] not in ['none', 'cave']): + if (world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' + or world.pottery[player] not in ['none', 'cave']): rom.write_byte(0x151f1, 2) rom.write_byte(0x15270, 2) sanctuary = world.get_region('Sanctuary', player) @@ -745,7 +743,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): return region.is_light_world and not region.is_dark_world # dark world spawns - sanc_name = 'Sanctuary' if world.mode[player] != 'inverted' else 'Dark Sanctuary Hint' + sanc_name = 'Sanctuary' if not world.is_dark_chapel_start(player) else 'Dark Sanctuary Hint' sanc_region = world.get_region(sanc_name, player) if should_be_bunny(sanc_region, world.mode[player]): rom.write_bytes(0x13fff2, [0x12, 0x00 if sanc_name == 'Sanctuary' else 0x01]) @@ -1079,7 +1077,11 @@ def patch_rom(world, rom, player, team, is_mystery=False): 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel ]) - # item GFX changes + # item property changes + if world.prizeshuffle[player] != 'none': + # allows prizes to contribute to collection rate + write_int16s(rom, snes_to_pc(0x22C06E), [0x01]*3) # pendants + write_int16s(rom, snes_to_pc(0x22C160), [0x81]*7) # crystals if world.bombbag[player]: rom.write_byte(snes_to_pc(0x22C8A4), 0xE0) # use new bomb bag gfx rom.write_byte(snes_to_pc(0x22BD52), 0x02) @@ -1264,94 +1266,152 @@ def patch_rom(world, rom, player, team, is_mystery=False): # Bitfield - enable text box to show with free roaming items # - # ---o bmcs + # --po bmcs + # p - enabled for non-prize crystals # o - enabled for outside dungeon items # b - enabled for inside big keys # m - enabled for inside maps # c - enabled for inside compasses # s - enabled for inside small keys - rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) + rom.write_byte(0x18016A, 0x10 | ((0x20 if world.prizeshuffle[player] == 'wild' else 0x00) + | (0x01 if world.keyshuffle[player] == 'wild' else 0x00) | (0x02 if world.compassshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] else 0x00) | (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count - compass_mode = 0x00 + compass_mode = 0x80 if world.compassshuffle[player] else 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': - compass_mode = 0x00 # Currently must be off if timer is on, because they use same HUD location - rom.write_byte(0x18003C, 0x00) + pass elif world.dungeon_counters[player] == 'on': - compass_mode = 0x02 # always on + compass_mode |= 0x02 # always on elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']): - compass_mode = 0x01 # show on pickup - if (world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default') or world.owMixed[player]: - compass_mode |= 0x80 # turn on locating dungeons - if world.overworld_map[player] == 'compass': - compass_mode |= 0x20 # show icon if compass is collected, 0x00 for maps - if world.compassshuffle[player]: - compass_mode |= 0x40 # dungeon item that enables icon is wild - elif world.overworld_map[player] == 'map' or world.owMixed[player]: - if world.mapshuffle[player]: - compass_mode |= 0x40 # dungeon item that enables icon is wild - - if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': - x_map_position_generic = [0x3c0, 0xbc0, 0x7c0, 0x1c0, 0x5c0, 0xdc0, 0x7c0, 0xbc0, 0x9c0, 0x3c0] - for idx, x_map in enumerate(x_map_position_generic): - rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map)) - rom.write_bytes(0x53e16+idx*2, int16_as_bytes(0xFC0)) - elif world.overworld_map[player] == 'default': - # disable HC/AT/GT icons - if not world.owMixed[player]: - rom.write_bytes(0x53E8A, int16_as_bytes(0xFF00)) # GT - rom.write_bytes(0x53E8C, int16_as_bytes(0xFF00)) # AT - rom.write_bytes(0x53E8E, int16_as_bytes(0xFF00)) # HC - for dungeon, portal_list in dungeon_portals.items(): - ow_map_index = dungeon_table[dungeon].map_index - if world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default': - vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', - 'Desert Palace': 'Desert Palace Entrance (North)', - 'Skull Woods': 'Skull Woods Final Section' - } - entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon - entrance = world.get_entrance(entrance_name, player) + compass_mode |= 0x01 # show on pickup + if world.overworld_map[player] == 'map': + compass_mode |= 0x10 # show icon if map is collected + elif world.overworld_map[player] == 'compass': + compass_mode |= 0x20 # show icon if compass is collected + if world.prizeshuffle[player] == 'wild': + compass_mode |= 0x40 # show icon if boss is defeated, hide if collected + rom.write_byte(0x18003C, compass_mode) + + def get_entrance_coords(ent): + if type(ent) is Location: + from OverworldShuffle import OWTileRegions + if ent.name == 'Hobo': + coords = (0xb80, 0xb80) + elif ent.name == 'Master Sword Pedestal': + coords = (0x06d, 0x070) else: - if world.shuffle[player] != 'vanilla': - if len(portal_list) == 1: - portal_idx = 0 - else: - if world.doorShuffle[player] not in ['vanilla', 'basic']: - # the random choice excludes sanctuary - portal_idx = next((i for i, elem in enumerate(portal_list) - if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) - else: - portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3}[dungeon] + owid = OWTileRegions[ent.parent_region.name] + if owid == 0x81: + coords = (0x220, 0xf40) else: - if dungeon in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: - portal_idx = -1 - elif len(portal_list) == 1: - portal_idx = 0 - else: - portal_idx = {'Desert Palace': 1, 'Skull Woods': 3, 'Turtle Rock': 0}[dungeon] - portal = world.get_portal(portal_list[0 if portal_idx == -1 else portal_idx], player) - entrance = portal.find_portal_entrance() - world_indicator = 0x01 if entrance.parent_region.type == RegionType.DarkWorld else 0x00 - coords = ow_prize_table[entrance.name] - # figure out compass entrances and what world (light/dark) - if world.overworld_map[player] != 'default' or world.owMixed[player]: - rom.write_bytes(0x53E36+ow_map_index*2, int16_as_bytes(coords[0])) - rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) - rom.write_byte(0x53EA6+ow_map_index, world_indicator) + owid = owid % 0x40 + coords = (0x200 * (owid % 0x08) + 0x100, 0x200 * int(owid / 0x08) + 0x100) + if owid in [0x00, 0x03, 0x05, 0x18, 0x1b, 0x1e, 0x30, 0x35]: + coords = (coords[0] + 0x100, coords[1] + 0x100) + else: + coords = ow_prize_table[ent.name] + coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1]) + return coords + if world.overworld_map[player] == 'default': + # disable HC/AT/GT icons + if not world.owMixed[player]: + write_int16(rom, snes_to_pc(0x0ABF52)+0x1A, 0x0000) # GT + write_int16(rom, snes_to_pc(0x0ABF52)+0x08, 0x0000) # AT + write_int16(rom, snes_to_pc(0x0ABF52)+0x00, 0x0000) # HC + for dungeon, portal_list in dungeon_portals.items(): + dungeon_index = dungeon_table[dungeon].dungeon_index + extra_map_index = dungeon_table[dungeon].extra_map_index + map_index = max(0, dungeon_index - 2) + + # write out dislocated coords + if map_index >= 0x02 and map_index < 0x18 and (world.overworld_map[player] != 'default' or world.prizeshuffle[player] == 'wild'): + owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47] + x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0] + y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0] + world_indicator = 0x0000 + idx = int((map_index-2)/2) + owid = owid_map[idx] + if owid != 0xFF: + if (owid < 0x40) == (world.is_tile_swapped(owid, player)): + world_indicator = 0x8000 + write_int16(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+4, world_indicator | x_map_position_generic[idx]) + write_int16(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+6, y_map_position_generic[idx]) + + # write out icon coord data + if world.prizeshuffle[player] == 'wild' and dungeon_table[dungeon].prize: + dungeon_obj = world.get_dungeon(dungeon, player) + entrance = dungeon_obj.prize.get_map_location() + coords = get_entrance_coords(entrance) + # prize location + write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) + if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default': + # TODO: I think this is logically the same as some of the vanilla stuff below + vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', + 'Desert Palace': 'Desert Palace Entrance (North)', + 'Skull Woods': 'Skull Woods Final Section' } + entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon + if world.is_atgt_swapped(player): + swap_entrances = { 'Agahnims Tower': 'Ganons Tower', + 'Ganons Tower': 'Agahnims Tower' } + entrance_name = swap_entrances[dungeon] if dungeon in swap_entrances else entrance_name + entrance = world.get_entrance(entrance_name, player) + else: + if len(portal_list) == 1: + portal_idx = 0 + else: + vanilla_portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3} + extra_map_offsets = {'Hyrule Castle': 0, 'Desert Palace': 0x12, 'Skull Woods': 0x20, 'Turtle Rock': 0x3E} + portal_idx = vanilla_portal_idx[dungeon] + offset = 0 + if (world.overworld_map[player] != 'default' and world.shuffle[player] not in ['vanilla', 'dungeonssimple'] + and (dungeon != 'Skull Woods' or world.shuffle[player] in ['district', 'insanity'])): + for i, elem in enumerate(portal_list): + if i != portal_idx and (elem != 'Sanctuary' or world.shuffle[player] in ['district', 'insanity']): + portal = world.get_portal(elem, player) + entrance = portal.find_portal_entrance() + coords = get_entrance_coords(entrance) + write_int16s(rom, snes_to_pc(0x8ABECA+extra_map_offsets[dungeon]+offset), coords) + offset += 4 + portal = world.get_portal(portal_list[portal_idx], player) + entrance = portal.find_portal_entrance() + coords = get_entrance_coords(entrance) + + # figure out compass entrances and what world (light/dark) + write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) + if world.prizeshuffle[player] != 'wild' and dungeon_table[dungeon].prize: + # prize location + write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) + + # Map reveals + reveal_bytes = { + "Hyrule Castle": 0xC000, + "Eastern Palace": 0x2000, + "Desert Palace": 0x1000, + "Tower of Hera": 0x0020, + "Agahnims Tower": 0x800, + "Palace of Darkness": 0x0200, + "Thieves Town": 0x0010, + "Skull Woods": 0x0080, + "Swamp Palace": 0x0400, + "Ice Palace": 0x0040, + "Misery Mire": 0x0100, + "Turtle Rock": 0x0008, + "Ganons Tower": 0x0004 + } # in crossed doors - flip the compass exists flags if world.doorShuffle[player] not in ['vanilla', 'basic']: + compass_exists = 0x0000 for dungeon, portal_list in dungeon_portals.items(): - ow_map_index = dungeon_table[dungeon].map_index - exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') - rom.write_byte(0x53E96+ow_map_index, 0x1 if exists_flag else 0x0) - - rom.write_byte(0x18003C, compass_mode) + dungeon_index = dungeon_table[dungeon].dungeon_index + if any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass'): + compass_exists |= reveal_bytes.get(dungeon, 0x0000) + write_int16(rom, snes_to_pc(0x0ABF6E), compass_exists) # Bitfield - enable free items to show up in menu # @@ -1362,38 +1422,21 @@ def patch_rom(world, rom, player, team, is_mystery=False): # b - Big Key # a - Small Key # - enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla' + enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] == 'wild' rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) | (0x02 if world.bigkeyshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00) | (0x08 if world.compassshuffle[player] else 0x00) # free roaming items in menu | (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon - # Map reveals - reveal_bytes = { - "Eastern Palace": 0x2000, - "Desert Palace": 0x1000, - "Tower of Hera": 0x0020, - "Palace of Darkness": 0x0200, - "Thieves Town": 0x0010, - "Skull Woods": 0x0080, - "Swamp Palace": 0x0400, - "Ice Palace": 0x0040, - "Misery Mire'": 0x0100, - "Turtle Rock": 0x0008, - } - def get_reveal_bytes(itemName): - locations = world.find_items(itemName, player) - if len(locations) < 1: - return 0x0000 - location = locations[0] - if location.parent_region and location.parent_region.dungeon: - return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000) + for dungeon in world.dungeons: + if dungeon.player == player and dungeon.prize and dungeon.prize.name == itemName: + return reveal_bytes.get(dungeon.name, 0x0000) return 0x0000 - write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal - write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle[player] else 0x0000) # Bomb Shop Reveal + write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant')) # Sahasrahla reveal + write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6')) # Bomb Shop Reveal rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == 'universal' else 0x00) # universal keys rom.write_byte(0x180175, 0x01 if world.bow_mode[player].startswith('retro') else 0x00) # rupee bow @@ -2050,7 +2093,7 @@ def write_strings(rom, world, player, team): entrances_to_hint.update(ItemEntrances) entrances_to_hint.update(ShopEntrances) entrances_to_hint.update(OtherEntrances) - if world.mode[player] != 'inverted': + if not world.is_dark_chapel_start(player): entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'district']: if world.shufflelinks[player]: @@ -2162,6 +2205,8 @@ def write_strings(rom, world, player, team): items_to_hint.extend(SmallKeys) if world.bigkeyshuffle[player]: items_to_hint.extend(BigKeys) + if world.prizeshuffle[player] == 'wild': + items_to_hint.extend(Prizes) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 @@ -2275,10 +2320,14 @@ def write_strings(rom, world, player, team): crystal5 = world.find_items('Crystal 5', player)[0] crystal6 = world.find_items('Crystal 6', player)[0] - tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5.hint_text, crystal6.hint_text) - greenpendant = world.find_items('Green Pendant', player)[0] - tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text + if world.prizeshuffle[player] == 'none': + (crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]]) + tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6) + tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant + else: + tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) + tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt[player] == 1 else 'You need %d crystals to enter.') % world.crystals_needed_for_gt[player] @@ -2368,10 +2417,10 @@ def write_strings(rom, world, player, team): # inverted spawn menu changes lh_text = "House" - if world.is_tile_swapped(0x2c, player): + if world.is_bombshop_start(player): lh_text = "Bomb Shop" sanc_text = "Sanctuary" - if world.mode[player] == 'inverted': + if world.is_dark_chapel_start(player): sanc_text = "Dark Chapel" tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n Mountain Cave\n{CHOICE2}" @@ -2468,6 +2517,8 @@ def set_inverted_mode(world, player, rom, inverted_buffer): rom.write_byte(snes_to_pc(0x0ABFBB), 0x90) # move mirror portal indicator to correct map (0xB0 normally) rom.write_byte(snes_to_pc(0x0280A6), 0xD0) # use starting point prompt instead of start at pyramid + if world.is_dark_chapel_start(player): + patch_shuffled_dark_sanc(world, rom, player) write_int16(rom, snes_to_pc(0x02D8D4), 0x112) # change sanctuary spawn point to dark sanc rom.write_bytes(snes_to_pc(0x02D8E8), [0x22, 0x22, 0x22, 0x23, 0x04, 0x04, 0x04, 0x05]) write_int16(rom, snes_to_pc(0x02D91A), 0x0400) @@ -2610,26 +2661,25 @@ def set_inverted_mode(world, player, rom, inverted_buffer): del world.data_tables[player].ow_enemy_table[0xab][5] # remove castle gate warp if world.is_tile_swapped(0x29, player): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) # frog pickup on contact - if world.is_tile_swapped(0x2c, player): - if world.is_bombshop_start(player): - rom.write_bytes(snes_to_pc(0x03F484), [0xFD, 0x4B, 0x68]) # place bed in bomb shop - - # spawn in bomb shop - patch_shuffled_bomb_shop(world, rom, player) - rom.write_byte(snes_to_pc(0x02D8D2), 0x1C) - rom.write_bytes(snes_to_pc(0x02D8E0), [0x23, 0x22, 0x23, 0x23, 0x18, 0x18, 0x18, 0x19]) - rom.write_byte(snes_to_pc(0x02D919), 0x18) - rom.write_byte(snes_to_pc(0x02D927), 0x23) - write_int16(rom, snes_to_pc(0x02D934), 0x2398) - rom.write_byte(snes_to_pc(0x02D943), 0x18) - write_int16(rom, snes_to_pc(0x02D950), 0x0087) - write_int16(rom, snes_to_pc(0x02D95E), 0x0081) - rom.write_byte(snes_to_pc(0x02D9A4), 0x53) + if world.is_bombshop_start(player): + rom.write_bytes(snes_to_pc(0x03F484), [0xFD, 0x4B, 0x68]) # place bed in bomb shop + + # spawn in bomb shop + patch_shuffled_bomb_shop(world, rom, player) + rom.write_byte(snes_to_pc(0x02D8D2), 0x1C) + rom.write_bytes(snes_to_pc(0x02D8E0), [0x23, 0x22, 0x23, 0x23, 0x18, 0x18, 0x18, 0x19]) + rom.write_byte(snes_to_pc(0x02D919), 0x18) + rom.write_byte(snes_to_pc(0x02D927), 0x23) + write_int16(rom, snes_to_pc(0x02D934), 0x2398) + rom.write_byte(snes_to_pc(0x02D943), 0x18) + write_int16(rom, snes_to_pc(0x02D950), 0x0087) + write_int16(rom, snes_to_pc(0x02D95E), 0x0081) + rom.write_byte(snes_to_pc(0x02D9A4), 0x53) - # disable custom exit on links house exit - rom.write_byte(snes_to_pc(0x02E225), 0x1C) - rom.write_byte(snes_to_pc(0x02DAEE), 0x1C) - rom.write_byte(snes_to_pc(0x02DB8C), 0x6C) + # disable custom exit on links house exit + rom.write_byte(snes_to_pc(0x02E225), 0x1C) + rom.write_byte(snes_to_pc(0x02DAEE), 0x1C) + rom.write_byte(snes_to_pc(0x02DB8C), 0x6C) if world.is_tile_swapped(0x2f, player): rom.write_bytes(snes_to_pc(0x1BC80D), [0xB2, 0x0B, 0x82]) # add warp under rock rom.write_byte(snes_to_pc(0x1BC590), 0x00) # remove secret portal @@ -2967,6 +3017,18 @@ BigKeys = ['Big Key (Eastern Palace)', 'Big Key (Ganons Tower)' ] +Prizes = ['Green Pendant', + 'Blue Pendant', + 'Red Pendant', + 'Crystal 1', + 'Crystal 2', + 'Crystal 3', + 'Crystal 4', + 'Crystal 5', + 'Crystal 6', + 'Crystal 7' + ] + hash_alphabet = [ "Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", diff --git a/Rules.py b/Rules.py index 27075c6f..1ad5607e 100644 --- a/Rules.py +++ b/Rules.py @@ -63,13 +63,15 @@ def set_rules(world, player): if world.goal[player] == 'dungeons': # require all dungeons to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has_beaten_aga(player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) + add_rule(world.get_location('Ganon', player), lambda state: state.has_beaten_aga(player) and state.has('Beat Agahnim 2', player) and state.has('Beat Boss', player, 10)) elif world.goal[player] in ['crystals', 'ganon']: add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player)) if world.goal[player] == 'ganon': # require aga2 to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) elif world.goal[player] in ['triforcehunt', 'trinity']: + if world.goal[player] == 'trinity': + add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player)) for location in world.get_region('Hyrule Castle Courtyard', player).locations: if location.name == 'Murahdahla': add_rule(location, lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) @@ -146,9 +148,11 @@ def set_rule(spot, rule): spot.access_rule = rule -def set_defeat_dungeon_boss_rule(location): - # Lambda required to defer evaluation of dungeon.boss since it will change later if boos shuffle is used - set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) +def set_defeat_dungeon_boss_rule(entrance): + # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used + set_rule(entrance, lambda state: entrance.parent_region.dungeon.boss.can_defeat(state)) + if entrance.parent_region.dungeon.name == 'Thieves Town': + add_rule(entrance, lambda state: entrance.parent_region.dungeon.boss.name != 'Blind' or state.has('Maiden Unmasked', entrance.player)) def set_always_allow(spot, rule): @@ -422,20 +426,17 @@ def global_rules(world, player): set_rule(world.get_entrance('Eastern Map Balcony Hook Path', player), lambda state: state.has('Hookshot', player)) # Boss rules. Same as below but no BK or arrow requirement. - set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player)) - set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Boss', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Eastern Palace Boss', player)) # Desert set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player)) - set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', player)) - set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Desert Palace Boss', player)) # Tower of Hera set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Hera Big Chest Hook Path', player), lambda state: state.has('Hookshot', player)) - set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Tower of Hera Boss', player)) # Castle Tower set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player)) @@ -450,8 +451,7 @@ def global_rules(world, player): set_rule(world.get_entrance('PoD Dark Pegs Right to Landing', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Turtle Party NW', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Turtle Party ES', player), lambda state: state.has('Hammer', player)) - set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Palace of Darkness Boss', player)) set_rule(world.get_entrance('Swamp Lobby Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Entrance Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) @@ -487,8 +487,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Waterway N', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player)) set_rule(world.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: state.has('Flippers', player)) - set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Swamp Palace Boss', player)) set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player)) @@ -511,8 +510,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player) or hidden_pits_rule(state)) set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), hidden_pits_rule) - set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Skull Woods Boss', player)) # blind can't have the small key? - not necessarily true anymore - but likely still @@ -527,8 +525,7 @@ def global_rules(world, player): # for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']: # set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player)) - set_rule(world.get_location('Thieves\' Town - Boss', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Boss', player).parent_region.dungeon.boss.can_defeat(state)) - set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state)) + set_defeat_dungeon_boss_rule(world.get_entrance('Thieves Town Boss', player)) set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player)) if is_trapped('Ice Lobby SE'): @@ -554,8 +551,7 @@ def global_rules(world, player): if is_trapped('Ice Switch Room NE'): set_rule(world.get_entrance('Ice Switch Room NE', player), lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player)) - set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Ice Palace Boss', player)) set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) @@ -585,8 +581,7 @@ def global_rules(world, player): # set_rule(world.get_entrance('Mire Dark Shooters SE', player), # lambda state: state.has('Cane of Somaria', player)) - set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Misery Mire Boss', player)) set_rule(world.get_entrance('TR Main Lobby Gap', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Lobby Ledge Gap', player), lambda state: state.has('Cane of Somaria', player)) @@ -616,8 +611,7 @@ def global_rules(world, player): set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.can_avoid_lasers(player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.can_avoid_lasers(player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.can_avoid_lasers(player)) - set_defeat_dungeon_boss_rule(world.get_location('Turtle Rock - Boss', player)) - set_defeat_dungeon_boss_rule(world.get_location('Turtle Rock - Prize', player)) + set_defeat_dungeon_boss_rule(world.get_entrance('Turtle Rock Boss', player)) set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player)) @@ -863,7 +857,7 @@ def global_rules(world, player): d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon for loc in [info.prize, f'{d_name} - Boss']: add_mc_rule(loc) - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') @@ -1687,7 +1681,8 @@ def set_bunny_rules(world, player, inverted): all_single_exit_dungeons = ['Eastern Palace', 'Tower of Hera', 'Castle Tower', 'Palace of Darkness', 'Swamp Palace', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Ganons Tower'] hmg_single_exit_dungeons = [d for d in all_single_exit_dungeons if d not in ['Tower of Hera', 'Misery Mire', 'Thieves Town']] bunny_impassable_caves = ['Bumper Cave (top)', 'Bumper Cave (bottom)', 'Two Brothers House', - 'Hookshot Cave (Middle)', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)'] + 'Hookshot Cave (Middle)', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)', + 'Death Mountain Return Cave (right)', 'Paradox Cave (Top)'] bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Old Man', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins', diff --git a/Text.py b/Text.py index 996f4483..08756f8a 100644 --- a/Text.py +++ b/Text.py @@ -2,6 +2,8 @@ from collections import OrderedDict import logging import re +import warnings +warnings.filterwarnings("ignore", category=SyntaxWarning) text_addresses = {'Pedestal': (0x180300, 256), 'Triforce': (0x180400, 256), @@ -96,9 +98,9 @@ Triforce_texts = [ "\n G G", " All your base\n are belong\n to us.", " You have ended\n the domination\n of Dr. Wily", - " Thanks for\n playing!!!", + " Thanks for\n playing!!!", "\n You Win!", - " Thank you!\n Your quest\n is over.", + " Thank you!\n Your quest\n is over.", " A winner\n is you!", "\n WINNER!!", "\n I'm sorry\n\nbut our princess is\n in another castle", @@ -645,7 +647,7 @@ class MultiByteCoreTextMapper(object): linespace = wrap line = lines.pop(0) - match = re.search('^\{[A-Z0-9_:]+\}$', line) + match = re.search(r'^\{[A-Z0-9_:]+\}$', line) if match: if line == '{PAGEBREAK}': if lineindex % 3 != 0: @@ -664,13 +666,13 @@ class MultiByteCoreTextMapper(object): while words: word = words.pop(0) - match = re.search('^(\{[A-Z0-9_:]+\}).*', word) + match = re.search(r'^(\{[A-Z0-9_:]+\}).*', word) if match: start_command = match.group(1) outbuf.extend(cls.special_commands[start_command]) word = word.replace(start_command, '') - match = re.search('(\{[A-Z0-9_:]+\})\.?$', word) + match = re.search(r'(\{[A-Z0-9_:]+\})\.?$', word) if match: end_command = match.group(1) word = word.replace(end_command, '') diff --git a/Utils.py b/Utils.py index 06a83a19..b0f64517 100644 --- a/Utils.py +++ b/Utils.py @@ -347,11 +347,11 @@ def update_deprecated_args(args): # Don't do: Yes # Do: No if "suppress_spoiler" in argVars: - args.create_spoiler = not args.suppress_spoiler in truthy + args.spoiler = 'none' # Don't do: No # Do: Yes if "create_spoiler" in argVars: - args.suppress_spoiler = not args.create_spoiler in truthy + args.spoiler = 'full' # ROM defaults to TRUE # Don't do: Yes diff --git a/build-app_version.py b/build-app_version.py index 6d63f3ca..69332cba 100644 --- a/build-app_version.py +++ b/build-app_version.py @@ -1,5 +1,5 @@ -from Main import __version__ as DRVersion +from OverworldShuffle import __version__ as OWVersion import os with(open(os.path.join("resources","app","meta","manifests","app_version.txt"),"w+")) as f: - f.write(DRVersion) + f.write(OWVersion) diff --git a/data/base2current.bps b/data/base2current.bps index 37faa3b9..0aa27aaf 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/docs/Customizer.md b/docs/Customizer.md index c881119f..7616a749 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -81,7 +81,7 @@ You may list each location for a player and the item you wish to place there. A This must be defined by player. Each player number should be listed with the appropriate section. Each section is a list of placement rules. Each placement rule has a specific type. -Supported Types: PlacementGroup, NotPlacmentGroup +Supported Types: PlacementGroup, NotPlacmentGroup, PreferredLocationGroup #### PlacementGroup @@ -89,7 +89,11 @@ You may define an item, and a list of locations. The locations may be weighted i #### NotPlacementGroup -You may define an item and a list of locations that an item should not be placed at. This will apply to all items of that type. The logic is considered for this. If it is otherwise impossible, the item will be considered for the listed locations. This is important for small key layouts mostly, but it will try other locations first. +You may define an item and a list of locations that an item should not be placed at. This will apply to all items of that type. The logic is considered for this. If it is otherwise impossible, the item will be considered for the listed locations. This is important for small key layouts mostly, but it will try other locations first. + +#### PreferredPlacementGroup + +You may define a list of items and a list of locations. Those items will be considered first for placements and the logic will attempt to place those items in those locations first. If there are more item than locations or vice versa, the leftover items or location will be treated normally. (Although, the leftover items will be placed earlier by the algorithm than those not listed) ### ow-edges diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index 38393165..08110a27 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -98,8 +98,8 @@ compass_shuffle: on: 1 off: 1 smallkey_shuffle: - on: 1 - off: 1 + wild: 1 + none: 1 bigkey_shuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index dbbc8c8b..eef2cccc 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -4,14 +4,14 @@ "action": "store_true", "type": "bool" }, - "create_spoiler": { - "action": "store_false", - "dest": "suppress_spoiler", - "type": "bool", - "help": "suppress" - }, - "suppress_spoiler": { - "action": "store_true" + "spoiler": { + "choices": [ + "none", + "settings", + "semi", + "full", + "debug" + ] }, "mystery": { "action": "store_true", @@ -387,6 +387,13 @@ "action": "store_true", "type": "bool" }, + "prizeshuffle": { + "choices": [ + "none", + "dungeon", + "wild" + ] + }, "keysanity": { "action": "store_true", "type": "bool", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 15dae0aa..80f479e8 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -61,7 +61,15 @@ }, "help": { "lang": [ "App Language, if available, defaults to English" ], - "create_spoiler": [ "Output a Spoiler File" ], + "spoiler": [ + "Spoiler File Options. (default: %(default)s)", + "None: No Spoiler", + "Meta: Meta information only about game. Intended for mystery settings", + "Settings: Only settings information", + "Semi: ", + "Full: Full spoiler generated", + "Debug: Includes debug information" + ], "bps": [ "Output BPS patches instead of ROMs"], "logic": [ "Select Enforcement of Item Requirements. (default: %(default)s)", @@ -342,6 +350,7 @@ "compassshuffle": [ "Compasses are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], + "prizeshuffle": [ "Prizes are no longer restricted to the bosses, but can be anywhere. (default: %(default)s)" ], "shopsanity": ["Shop contents are shuffle in the main item pool and other items can take their place. (default: %(default)s)"], "dropshuffle": [ "Controls how enemies drop items (default: %(default)s)", "None: Enemies drops prize packs or keys as normal", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 53ea2c74..a9541a0b 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -59,6 +59,10 @@ "randomizer.dungeon.smallkeyshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", + "randomizer.dungeon.prizeshuffle": "Prizes", + "randomizer.dungeon.prizeshuffle.none": "On Boss", + "randomizer.dungeon.prizeshuffle.dungeon": "In Dungeon", + "randomizer.dungeon.prizeshuffle.wild": "Randomized", "randomizer.dungeon.decoupledoors": "Decouple Doors", "randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs", @@ -236,7 +240,12 @@ "randomizer.generation.bps": "Create BPS Patches", - "randomizer.generation.createspoiler": "Create Spoiler Log", + "randomizer.generation.spoiler": "Create Spoiler Log", + "randomizer.generation.spoiler.none": "None", + "randomizer.generation.spoiler.settings": "Settings Only", + "randomizer.generation.spoiler.semi": "Semi (Entrances and Prizes)", + "randomizer.generation.spoiler.full": "Full", + "randomizer.generation.spoiler.debug": "Debug", "randomizer.generation.createrom": "Create Patched ROM", "randomizer.generation.calcplaythrough": "Calculate Playthrough", "randomizer.generation.print_custom_yaml": "Print Customizer File", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index b02dfa6f..268fee35 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -1,5 +1,17 @@ { "widgets": { + "prizeshuffle": { + "type": "selectbox", + "default": "default", + "options": [ + "none", + "dungeon", + "wild" + ], + "config": { + "padx": [20,0] + } + }, "key_logic_algorithm": { "type": "selectbox", "default": "default", diff --git a/resources/app/gui/randomize/generation/checkboxes.json b/resources/app/gui/randomize/generation/checkboxes.json index 6e377027..3eeab78b 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -2,7 +2,16 @@ "checkboxes": { "createrom": { "type": "checkbox" }, "bps": { "type": "checkbox" }, - "createspoiler": { "type": "checkbox" }, + "spoiler": { + "type": "selectbox", + "options": [ + "none", + "settings", + "semi", + "full", + "debug" + ] + }, "calcplaythrough": { "type": "checkbox" }, "print_custom_yaml": { "type": "checkbox" } } diff --git a/resources/app/meta/manifests/app_version.txt b/resources/app/meta/manifests/app_version.txt new file mode 100644 index 00000000..e69de29b diff --git a/resources/app/meta/manifests/binaries.json b/resources/app/meta/manifests/binaries.json new file mode 100644 index 00000000..620df72f --- /dev/null +++ b/resources/app/meta/manifests/binaries.json @@ -0,0 +1,7 @@ +[ + "DungeonRandomizer", + "Gui", + "MultiClient", + "MultiServer", + "Mystery" +] diff --git a/resources/app/meta/manifests/excluded_dlls.json b/resources/app/meta/manifests/excluded_dlls.json new file mode 100644 index 00000000..825d5999 --- /dev/null +++ b/resources/app/meta/manifests/excluded_dlls.json @@ -0,0 +1,34 @@ +[ + "conio", + "console", + "convert", + "datetime", + "debug", + "environment", + "errorhandling", + "file", + "filesystem", + "handle", + "heap", + "interlocked", + "libraryloader", + "locale", + "localization", + "math", + "memory", + "namedpipe", + "process", + "processenvironment", + "processthreads", + "profile", + "rtlsupport", + "runtime", + "stdio", + "string", + "synch", + "sysinfo", + "time", + "timezone", + "util", + "utility" +] diff --git a/resources/app/meta/manifests/pip_requirements.txt b/resources/app/meta/manifests/pip_requirements.txt index faa3c48f..26bede61 100644 --- a/resources/app/meta/manifests/pip_requirements.txt +++ b/resources/app/meta/manifests/pip_requirements.txt @@ -1,7 +1,8 @@ aenum +aioconsole +colorama +distro fast-enum python-bps-continued -colorama -aioconsole +pyyaml websockets -pyyaml \ No newline at end of file diff --git a/resources/ci/common/common.py b/resources/ci/common/common.py index b89a266b..5329c768 100644 --- a/resources/ci/common/common.py +++ b/resources/ci/common/common.py @@ -1,6 +1,11 @@ import os # for env vars import stat # file statistics import sys # default system info +try: + import distro +except ModuleNotFoundError as e: + pass + from my_path import get_py_path global UBUNTU_VERSIONS @@ -8,15 +13,20 @@ global DEFAULT_EVENT global DEFAULT_REPO_SLUG global FILENAME_CHECKS global FILESIZE_CHECK -UBUNTU_VERSIONS = { - "latest": "focal", - "20.04": "focal", - "18.04": "bionic", - "16.04": "xenial" -} +# GitHub Hosted Runners +# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories +# ubuntu: 22.04, 20.04 +# windows: 2022, 2019 +# macos: 14, 13, 12, 11 DEFAULT_EVENT = "event" DEFAULT_REPO_SLUG = "miketrethewey/ALttPDoorRandomizer" -FILENAME_CHECKS = [ "Gui", "DungeonRandomizer" ] +FILENAME_CHECKS = [ + "DungeonRandomizer", + "Gui", + "MultiClient", + "MultiServer", + "Mystery" +] FILESIZE_CHECK = (6 * 1024 * 1024) # 6MB # take number of bytes and convert to string with units measure @@ -38,12 +48,19 @@ def prepare_env(): global DEFAULT_REPO_SLUG env = {} - # get app version + # get app version APP_VERSION = "" - APP_VERSION_FILE = os.path.join(".","resources","app","meta","manifests","app_version.txt") - if os.path.isfile(APP_VERSION_FILE): - with open(APP_VERSION_FILE,"r") as f: - APP_VERSION = f.readlines()[0].strip() + APP_VERSION_FILES = [ + os.path.join(".","resources","app","meta","manifests","app_version.txt"), + os.path.join("..","build","app_version.txt") + ] + for app_version_file in APP_VERSION_FILES: + if os.path.isfile(app_version_file): + with open(app_version_file,"r") as f: + lines = f.readlines() + if len(lines) > 0: + APP_VERSION = lines[0].strip() + # ci data env["CI_SYSTEM"] = os.getenv("CI_SYSTEM","") # py data @@ -96,9 +113,11 @@ def prepare_env(): OS_VERSION = OS_NAME[OS_NAME.find('-')+1:] OS_NAME = OS_NAME[:OS_NAME.find('-')] if OS_NAME == "linux" or OS_NAME == "ubuntu": - if OS_VERSION in UBUNTU_VERSIONS: - OS_VERSION = UBUNTU_VERSIONS[OS_VERSION] - OS_DIST = OS_VERSION + try: + if distro.codename() != "": + OS_DIST = distro.codename() + except NameError as e: + pass if OS_VERSION == "" and not OS_DIST == "" and not OS_DIST == "notset": OS_VERSION = OS_DIST @@ -111,7 +130,7 @@ def prepare_env(): # if the app version didn't have the build number, add it # set to . if env["BUILD_NUMBER"] not in GITHUB_TAG: - GITHUB_TAG += '.' + env["BUILD_NUMBER"] + GITHUB_TAG += ".r" + env["BUILD_NUMBER"] env["GITHUB_TAG"] = GITHUB_TAG env["OS_NAME"] = OS_NAME diff --git a/resources/ci/common/get_get_pip.py b/resources/ci/common/get_get_pip.py index a0e127ba..1de2898b 100644 --- a/resources/ci/common/get_get_pip.py +++ b/resources/ci/common/get_get_pip.py @@ -10,7 +10,7 @@ def get_get_pip(PY_VERSION): try: import pip except ImportError: - print("Getting pip getter!") + print("🟡Getting pip getter!") #make the request! url = "https://bootstrap.pypa.io/get-pip.py" context = ssl._create_unverified_context() @@ -40,7 +40,7 @@ def get_get_pip(PY_VERSION): if float(PY_VERSION) > 0: PYTHON_EXECUTABLE = "py" - print("Getting pip!") + print("🟡Getting pip!") args = [ env["PYTHON_EXE_PATH"] + PYTHON_EXECUTABLE, '-' + str(PY_VERSION), @@ -58,6 +58,6 @@ if __name__ == "__main__": try: import pip - print("pip is installed") + print("🟢pip is installed") except ImportError: get_get_pip(PY_VERSION) diff --git a/resources/ci/common/get_pipline.py b/resources/ci/common/get_pipline.py new file mode 100644 index 00000000..edc5714a --- /dev/null +++ b/resources/ci/common/get_pipline.py @@ -0,0 +1,440 @@ +# import modules +import common # app common functions + +import json # json manipulation +import os # for os data, filesystem manipulation +import subprocess # for running shell commands +import sys # for system commands +import traceback # for errors + +# get env +env = common.prepare_env() # get environment variables + +# width for labels +WIDTH = 70 + +# bucket for cli args +args = [] + +# pip exe path +PIPEXE = "" + +# py exe path +# py version +# py minor version +PYTHON_EXECUTABLE = os.path.splitext(sys.executable.split(os.path.sep).pop())[0] # get command to run python +PYTHON_VERSION = sys.version.split(" ")[0] +PYTHON_MINOR_VERSION = '.'.join(PYTHON_VERSION.split(".")[:2]) + +# pip string version +# pip float version +PIP_VERSION = "" +PIP_FLOAT_VERSION = 0 + +# success +SUCCESS = False +# bucket for versions +VERSIONS = {} + +# process module output +# read output from installing +# print relevant info +# print unknown stuff +def process_module_output(lines): + for line in lines: + # if there's an error, print it and bail + if "status 'error'" in line.strip(): + print( + "🔴[%s] %s" + % + ( + "_", + line.strip() + ) + ) + return + # sys.exit(1) + # if it's already satisfied or building a wheel, print version data + elif "already satisfied" in line or \ + "Building wheel" in line or \ + "Created wheel" in line: + + modulename = print_module_line(line) + + if "=" not in modulename and VERSIONS[modulename]["installed"] != VERSIONS[modulename]["latest"]: + # install modules from list + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "--upgrade", + f"{modulename}" + ], + capture_output=True, + text=True + ) + # if there's output + if ret.stdout.strip(): + process_module_output(ret.stdout.strip().split("\n")) + + # ignore lines about certain things + elif "Attempting uninstall" in line or \ + "Collecting" in line or \ + "Downloading" in line or \ + "eta 0:00:00" in line or \ + "Found existing" in line or \ + "Installing collected" in line or \ + "Preparing metadata" in line or \ + "Successfully built" in line or \ + "Successfully installed" in line or \ + "Successfully uninstalled" in line or \ + "Stored in" in line or \ + "Uninstalling " in line or \ + "Using cached" in line: + pass + # else, I don't know what it is, print it + else: + print(line.strip()) + print("") + +# print module line +# name, installed version, latest version +def print_module_line(line): + global VERSIONS + # is it already installed? + satisfied = line.strip().split(" in ") + # get the installed version + sver = ((len(satisfied) > 1) and satisfied[1].split("(").pop().replace(")", "")) or "" + + # if we're making a wheel + if "Created wheel" in line: + line = line.strip().split(':') + satisfied = [line[0]] + sver = line[1].split('-')[1] + + # get module name + modulename = satisfied[0].replace("Requirement already satisfied: ", "") + # save info for later use + VERSIONS[modulename] = { + "installed": sver, + "latest": (sver and get_module_version(satisfied[0].split(" ")[-1])).strip() or "" + } + + # print what we found + print( + ( + "[%s] %s\t%s\t%s" + % + ( + "Building wheel" in line and '.' or "X", + satisfied[0].ljust(len("Requirement already satisfied: ") + len("python-bps-continued")), + VERSIONS[modulename]["installed"], + VERSIONS[modulename]["latest"] + ) + ) + ) + # return the name of this module + return modulename + +# get module version +# get installed version +def get_module_version(module): + # pip index versions [module] // >= 21.2 + # pip install [module]== // >= 21.1 + # pip install --use-deprecated=legacy-resolver [module]== // >= 20.3 + # pip install [module]== // >= 9.0 + # pip install [module]==blork // < 9.0 + global args + global PIPEXE + global PIP_FLOAT_VERSION + ret = "" + ver = "" + + # based on version of pip, get the installation status of a module + if float(PIP_FLOAT_VERSION) >= 21.2: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "index", + "versions", + module + ], + capture_output=True, + text=True + ) + lines = ret.stdout.strip().split("\n") + lines = lines[2::] + vers = (list(map(lambda x: x.split(' ')[-1], lines))) + if len(vers) > 1: + ver = vers[1] + elif float(PIP_FLOAT_VERSION) >= 21.1: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + f"{module}==" + ], + capture_output=True, + text=True + ) + elif float(PIP_FLOAT_VERSION) >= 20.3: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "--use-deprecated=legacy-resolver", + f"{module}==" + ], + capture_output=True, + text=True + ) + elif float(PIP_FLOAT_VERSION) >= 9.0: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + f"{module}==" + ], + capture_output=True, + text=True + ) + elif float(PIP_FLOAT_VERSION) < 9.0: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + f"{module}==blork" + ], + capture_output=True, + ext=True + ) + + # if ver == "" and ret.stderr.strip(): + # ver = (ret.stderr.strip().split("\n")[0].split(",")[-1].replace(')', '')).strip() + + # return what we found + return ver + +# get python info +def python_info(): + global args + global PYTHON_VERSION + + # get python debug info + ret = subprocess.run([*args, "--version"], capture_output=True, text=True) + if ret.stdout.strip(): + PYTHON_VERSION = ret.stdout.strip().split(" ")[1] + PY_STRING = ( + "%s\t%s\t%s" + % + ( + ((isinstance(args[0], list) and " ".join( + args[0])) or args[0]).strip(), + PYTHON_VERSION, + sys.platform + ) + ) + print(PY_STRING) + print('.' * WIDTH) + +# get pip info +def pip_info(): + global args + global PIPEXE + global PIPEXE + global VERSIONS + + # get pip debug info + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "--version" + ], + capture_output=True, + text=True + ) + if ret.stdout.strip(): + if " from " in ret.stdout.strip(): + PIP_VERSION = ret.stdout.strip().split(" from ")[0].split(" ")[1] + if PIP_VERSION: + b, f, a = PIP_VERSION.partition('.') + global PIP_FLOAT_VERSION + PIP_FLOAT_VERSION = b+f+a.replace('.', '') + PIP_LATEST = get_module_version("pip") + + VERSIONS["py"] = { + "version": PYTHON_VERSION, + "platform": sys.platform + } + VERSIONS["pip"] = { + "version": [ + PIP_VERSION, + PIP_FLOAT_VERSION + ], + "latest": PIP_LATEST + } + + PIP_STRING = ( + "%s\t%s\t%s\t%s\t%s\t%s" + % + ( + ((isinstance(args[0], list) and " ".join( + args[0])) or args[0]).strip(), + PYTHON_VERSION, + sys.platform, + PIPEXE, + PIP_VERSION, + PIP_LATEST + ) + ) + print(PIP_STRING) + print('.' * WIDTH) + +# upgrade pip +def pip_upgrade(): + global args + global PIPEXE + + # upgrade pip + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "--upgrade", "pip" + ], + capture_output=True, + text=True + ) + # get output + if ret.stdout.strip(): + # if it's not already satisfied, update it + if "already satisfied" not in ret.stdout.strip(): + print(ret.stdout.strip()) + pip_info() + +# install modules +def install_modules(): + global args + global PIPEXE + global SUCCESS + + # install modules from list + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "-r", + os.path.join( + ".", + "resources", + "app", + "meta", + "manifests", + "pip_requirements.txt" + ) + ], + capture_output=True, + text=True + ) + + # if there's output + if ret.stdout.strip(): + process_module_output(ret.stdout.strip().split("\n")) + manifests_path = os.path.join(".", "resources", "user", "meta", "manifests") + if not os.path.isdir(manifests_path): + os.makedirs(manifests_path) + + with open(os.path.join(manifests_path, "settings.json"), "w+") as settings: + settings.write( + json.dumps( + { + "py": args, + "pip": PIPEXE, + "pipline": " ".join(args) + " -m " + PIPEXE, + "versions": VERSIONS + }, + indent=2 + ) + ) + with open(os.path.join(manifests_path, "pipline.txt"), "w+") as settings: + settings.write(" ".join(args) + " -m " + PIPEXE) + SUCCESS = True + + +def main(): + global args + global PIPEXE + global SUCCESS + # print python debug info + heading = ( + "%s-%s-%s" + % + ( + PYTHON_EXECUTABLE, + PYTHON_VERSION, + sys.platform + ) + ) + print(heading) + print('=' * WIDTH) + + # figure out pip executable + PIPEXE = "pip" if "windows" in env["OS_NAME"] else "pip3" + PIPEXE = "pip" if "osx" in env["OS_NAME"] and "actions" in env["CI_SYSTEM"] else PIPEXE + + PIP_VERSION = "" # holder for pip's version + + SUCCESS = False + # foreach py executable + for PYEXE in ["py", "python3", "python"]: + if SUCCESS: + continue + + args = [] + # if it's the py launcher, specify the version + if PYEXE == "py": + PYEXE = [PYEXE, "-" + PYTHON_MINOR_VERSION] + # if it ain't windows, skip it + if "windows" not in env["OS_NAME"]: + continue + + # build executable command + if isinstance(PYEXE, list): + args = [*PYEXE] + else: + args = [PYEXE] + + try: + python_info() + + # foreach pip executable + for PIPEXE in ["pip3", "pip"]: + pip_info() + pip_upgrade() + install_modules() + + # if something else went fucky, print it + except Exception as e: + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/resources/ci/common/get_upx.py b/resources/ci/common/get_upx.py index 73c6b6f3..16982aaf 100644 --- a/resources/ci/common/get_upx.py +++ b/resources/ci/common/get_upx.py @@ -21,10 +21,11 @@ if not os.path.isdir(os.path.join(".","upx")): UPX_FILE = UPX_SLUG + ".tar.xz" UPX_URL = "https://github.com/upx/upx/releases/download/v" + UPX_VERSION + '/' + UPX_FILE + # if it's not macos if "osx" not in env["OS_NAME"]: - print("Getting UPX: " + UPX_FILE) + # download UPX with open(os.path.join(".",UPX_FILE),"wb") as upx: UPX_REQ = urllib.request.Request( UPX_URL, @@ -34,8 +35,10 @@ if not os.path.isdir(os.path.join(".","upx")): UPX_DATA = UPX_REQ.read() upx.write(UPX_DATA) + # extract UPX unpack_archive(UPX_FILE,os.path.join(".")) + # move UPX os.rename(os.path.join(".",UPX_SLUG),os.path.join(".","upx")) os.remove(os.path.join(".",UPX_FILE)) diff --git a/resources/ci/common/list_actions.py b/resources/ci/common/list_actions.py new file mode 100644 index 00000000..ec4caf5f --- /dev/null +++ b/resources/ci/common/list_actions.py @@ -0,0 +1,168 @@ +# pylint: disable=invalid-name +''' +List GitHub Actions versions used and latest versions +''' +import json +import os +import ssl +import urllib.request +import yaml +from json.decoder import JSONDecodeError + +allACTIONS = {} +listACTIONS = [] + +VER_WIDTH = 10 +NAME_WIDTH = 40 +LINE_WIDTH = 1 + NAME_WIDTH + 5 + VER_WIDTH + 5 + VER_WIDTH + 1 + +def process_walk(key, node): + ''' + Process walking through the array + ''' + global allACTIONS + global listACTIONS + if key == "uses": + action = node.split('@') + version = "" + if '@' in node: + version = action[1] + action = action[0] + if action not in allACTIONS: + allACTIONS[action] = { + "versions": [], + "latest": "" + } + allACTIONS[action]["versions"].append(version) + allACTIONS[action]["versions"] = list( + set( + allACTIONS[action]["versions"] + ) + ) + listACTIONS.append(node) + + +def walk(key, node): + ''' + How to walk through the array + ''' + if isinstance(node, dict): + return {k: walk(k, v) for k, v in node.items()} + elif isinstance(node, list): + return [walk(key, x) for x in node] + else: + return process_walk(key, node) + + +for r, d, f in os.walk(os.path.join(".", ".github")): + if "actions" in r or "workflows" in r: + for filename in f: + # if it's not a YAML or it's turned off, skip it + if (".yml" not in filename and ".yaml" not in filename) or (".off" in filename): + continue + listACTIONS = [] + # print filename + filename_line = "-" * (len(os.path.join(r, filename)) + 2) + print( + " " + + filename_line + + " " + ) + print("| " + os.path.join(r, filename) + " |") + # read the file + with(open(os.path.join(r, filename), "r", encoding="utf-8")) as yamlFile: + print( + "|" + + filename_line + + "-" + + ("-" * (LINE_WIDTH - len(filename_line) + 1)) + + " " + ) + yml = yaml.safe_load(yamlFile) + walk("uses", yml) + dictACTIONS = {} + for k in sorted(list(set(listACTIONS))): + action = k.split('@')[0] + version = k.split('@')[1] if '@' in k else "" + latest = "" + # if it's not a location action, get the latest version number + if "./." not in action: + apiURL = f"https://api.github.com/repos/{action}/releases/latest" + if True: + apiReq = None + try: + apiReq = urllib.request.urlopen( + apiURL, + context=ssl._create_unverified_context() + ) + except urllib.error.HTTPError as e: + if e.code != 403: + print(e.code, apiURL) + if apiReq: + apiRes = {} + try: + apiRes = json.loads( + apiReq.read().decode("utf-8")) + except JSONDecodeError as e: + raise ValueError("API Request failed: " + apiURL) + if apiRes: + latest = apiRes["tag_name"] if "tag_name" in apiRes else "" + if latest != "": + allACTIONS[action]["latest"] = latest + dictACTIONS[action] = version + # print action name and version info + for action, version in dictACTIONS.items(): + print( + "| " + \ + f"{action.ljust(NAME_WIDTH)}" + \ + "\t" + \ + f"{(version or 'N/A').ljust(VER_WIDTH)}" + \ + "\t" + \ + f"{(allACTIONS[action]['latest'] or 'N/A').ljust(VER_WIDTH)}" + \ + " |" + ) + print( + " " + + ("-" * (LINE_WIDTH + 2)) + + " " + ) + print("") + +# print outdated versions summary +first = True +outdated = False +for action, actionData in allACTIONS.items(): + if len(actionData["versions"]) > 0: + if actionData["latest"] != "" and actionData["versions"][0] != actionData["latest"]: + outdated = True + if first: + first = False + filename_line = "-" * (len("| Outdated |")) + print( + " " + + filename_line + + " " + ) + print("| Outdated |") + print( + "|" + + filename_line + + "-" + + ("-" * (LINE_WIDTH - len(filename_line) + 1)) + + " " + ) + print( + "| " + \ + f"{action.ljust(40)}" + \ + "\t" + \ + f"{(','.join(actionData['versions']) or 'N/A').ljust(10)}" + \ + "\t" + \ + f"{actionData['latest'].ljust(10)}" + \ + " |" + ) +if outdated: + print( + " " + + ("-" * (LINE_WIDTH + 2)) + + " " + ) diff --git a/resources/ci/common/prepare_appversion.py b/resources/ci/common/prepare_appversion.py index bd26318e..0f413298 100644 --- a/resources/ci/common/prepare_appversion.py +++ b/resources/ci/common/prepare_appversion.py @@ -5,12 +5,12 @@ from shutil import copy # file manipulation env = common.prepare_env() # set tag to app_version.txt -if not env["GITHUB_TAG"] == "": - with open(os.path.join(".","resources","app","meta","manifests","app_version.txt"),"w+") as f: - _ = f.read() - f.seek(0) - f.write(env["GITHUB_TAG"]) - f.truncate() +# if not env["GITHUB_TAG"] == "": +# with open(os.path.join(".","resources","app","meta","manifests","app_version.txt"),"w+") as f: +# _ = f.read() +# f.seek(0) +# f.write(env["GITHUB_TAG"]) +# f.truncate() if not os.path.isdir(os.path.join("..","build")): os.mkdir(os.path.join("..","build")) diff --git a/resources/ci/common/prepare_binary.py b/resources/ci/common/prepare_binary.py index ff9b7c99..4d9ac5e4 100644 --- a/resources/ci/common/prepare_binary.py +++ b/resources/ci/common/prepare_binary.py @@ -1,42 +1,48 @@ -import distutils.dir_util # for copying trees +""" +Locate and prepare binary builds +""" +# import distutils.dir_util # for copying trees import os # for env vars -import stat # for file stats -import subprocess # do stuff at the shell level +# import stat # for file stats +# import subprocess # do stuff at the shell level import common -from shutil import copy, make_archive, move, rmtree # file manipulation +from shutil import move # file manipulation env = common.prepare_env() # make dir to put the binary in if not os.path.isdir(os.path.join("..","artifact")): - os.mkdir(os.path.join("..","artifact")) + os.mkdir(os.path.join("..","artifact")) BUILD_FILENAME = "" # list executables BUILD_FILENAME = common.find_binary('.') if BUILD_FILENAME == "": - BUILD_FILENAME = common.find_binary(os.path.join("..","artifact")) + BUILD_FILENAME = common.find_binary(os.path.join("..","artifact")) if isinstance(BUILD_FILENAME,str): - BUILD_FILENAME = list(BUILD_FILENAME) + BUILD_FILENAME = list(BUILD_FILENAME) BUILD_FILENAMES = BUILD_FILENAME +print("OS Name: " + env["OS_NAME"]) +print("OS Version: " + env["OS_VERSION"]) +print("OS Distribution: " + env["OS_DIST"]) +print("") for BUILD_FILENAME in BUILD_FILENAMES: - DEST_FILENAME = common.prepare_filename(BUILD_FILENAME) + DEST_FILENAME = common.prepare_filename(BUILD_FILENAME) - print("OS Name: " + env["OS_NAME"]) - print("OS Version: " + env["OS_VERSION"]) - print("Build Filename: " + BUILD_FILENAME) - print("Dest Filename: " + DEST_FILENAME) - if not BUILD_FILENAME == "": - print("Build Filesize: " + common.file_size(BUILD_FILENAME)) - else: - exit(1) + print("Build Filename: " + BUILD_FILENAME) + print("Dest Filename: " + DEST_FILENAME) + if not BUILD_FILENAME == "": + print("Build Filesize: " + common.file_size(BUILD_FILENAME)) + else: + exit(1) - if not BUILD_FILENAME == "": - move( - os.path.join(".",BUILD_FILENAME), - os.path.join("..","artifact",BUILD_FILENAME) - ) + if not BUILD_FILENAME == "": + move( + os.path.join(".",BUILD_FILENAME), + os.path.join("..","artifact",BUILD_FILENAME) + ) + print("") diff --git a/resources/ci/common/prepare_release.py b/resources/ci/common/prepare_release.py index c3869f2d..8bab9de4 100644 --- a/resources/ci/common/prepare_release.py +++ b/resources/ci/common/prepare_release.py @@ -101,7 +101,8 @@ if len(BUILD_FILENAMES) > 0: # .zip if windows # .tar.gz otherwise if len(BUILD_FILENAMES) > 1: - ZIP_FILENAME = os.path.join("..","deploy",env["REPO_NAME"]) + # ZIP_FILENAME = os.path.join("..","deploy",env["REPO_NAME"]) + ZIP_FILENAME = os.path.join("..","deploy","ALttPOverworldRandomizer") else: ZIP_FILENAME = os.path.join("..","deploy",os.path.splitext(BUILD_FILENAME)[0]) if env["OS_NAME"] == "windows": @@ -124,15 +125,15 @@ for BUILD_FILENAME in BUILD_FILENAMES: print("Build Filename: " + BUILD_FILENAME) print("Build Filesize: " + common.file_size(BUILD_FILENAME)) else: - print("No Build to prepare: " + BUILD_FILENAME) + print("🟡No Build to prepare: " + BUILD_FILENAME) if not ZIP_FILENAME == "": print("Zip Filename: " + ZIP_FILENAME) print("Zip Filesize: " + common.file_size(ZIP_FILENAME)) else: - print("No Zip to prepare: " + ZIP_FILENAME) + print("🟡No Zip to prepare: " + ZIP_FILENAME) -print("Git tag: " + env["GITHUB_TAG"]) +print("App Version: " + env["GITHUB_TAG"]) if (len(BUILD_FILENAMES) == 0) or (ZIP_FILENAME == ""): exit(1) diff --git a/source/DungeonRandomizer.spec b/source/DungeonRandomizer.spec deleted file mode 100644 index beb2ecfc..00000000 --- a/source/DungeonRandomizer.spec +++ /dev/null @@ -1,68 +0,0 @@ -# -*- mode: python -*- - -import sys - -block_cipher = None -console = True # <--- change this to True to enable command prompt when the app runs - -if sys.platform.find("mac") or sys.platform.find("osx"): - console = False - -BINARY_SLUG = "DungeonRandomizer" - -def recurse_for_py_files(names_so_far): - returnvalue = [] - for name in os.listdir(os.path.join(*names_so_far)): - if name != "__pycache__": - subdir_name = os.path.join(*names_so_far, name) - if os.path.isdir(subdir_name): - new_name_list = names_so_far + [name] - for filename in os.listdir(os.path.join(*new_name_list)): - base_file,file_extension = os.path.splitext(filename) - if file_extension == ".py": - new_name = ".".join(new_name_list+[base_file]) - if not new_name in returnvalue: - returnvalue.append(new_name) - returnvalue.extend(recurse_for_py_files(new_name_list)) - returnvalue.append("PIL._tkinter_finder") #Linux needs this - return returnvalue - -hiddenimports = [] -binaries = [] - -a = Analysis([f"../{BINARY_SLUG}.py"], - pathex=[], - binaries=binaries, - datas=[('../data/', 'data/')], - hiddenimports=hiddenimports, - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) - -# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller -excluded_binaries = [ - 'VCRUNTIME140.dll', - 'ucrtbase.dll', - 'msvcp140.dll', - 'mfc140u.dll'] -a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name=BINARY_SLUG, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - runtime_tmpdir=None, - console=console ) diff --git a/source/Gui.spec b/source/Gui.spec deleted file mode 100644 index 8b140a75..00000000 --- a/source/Gui.spec +++ /dev/null @@ -1,69 +0,0 @@ -# -*- mode: python -*- - -import sys - -block_cipher = None -console = True # <--- change this to True to enable command prompt when the app runs - -if sys.platform.find("mac") or sys.platform.find("osx"): - console = False - -BINARY_SLUG = "Gui" - -def recurse_for_py_files(names_so_far): - returnvalue = [] - for name in os.listdir(os.path.join(*names_so_far)): - if name != "__pycache__": - subdir_name = os.path.join(*names_so_far, name) - if os.path.isdir(subdir_name): - new_name_list = names_so_far + [name] - for filename in os.listdir(os.path.join(*new_name_list)): - base_file,file_extension = os.path.splitext(filename) - if file_extension == ".py": - new_name = ".".join(new_name_list+[base_file]) - if not new_name in returnvalue: - returnvalue.append(new_name) - returnvalue.extend(recurse_for_py_files(new_name_list)) - returnvalue.append("PIL._tkinter_finder") #Linux needs this - return returnvalue - -hiddenimports = [] -binaries = [] - -a = Analysis([f"../{BINARY_SLUG}.py"], - pathex=[], - binaries=binaries, - datas=[('../data/', 'data/')], - hiddenimports=hiddenimports, - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) - -# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller -excluded_binaries = [ - 'VCRUNTIME140.dll', - 'ucrtbase.dll', - 'msvcp140.dll', - 'mfc140u.dll'] -a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name=BINARY_SLUG, - debug=False, - bootloader_ignore_signals=False, - icon='../data/ER.ico', - strip=False, - upx=True, - runtime_tmpdir=None, - console=console ) diff --git a/source/Template.spec b/source/Template.spec new file mode 100644 index 00000000..87fb999a --- /dev/null +++ b/source/Template.spec @@ -0,0 +1,98 @@ +# -*- mode: python -*- + +import json +import os +import sys +from json.decoder import JSONDecodeError +from PyInstaller.utils.hooks import collect_submodules + +block_cipher = None +console = False # <--- change this to True to enable command prompt when the app runs + +if sys.platform.find("mac") or sys.platform.find("osx"): + console = True + +BINARY_SLUG = "" + + +def recurse_for_py_files(names_so_far): + # get py files + returnvalue = [] + for name in os.listdir(os.path.join(*names_so_far)): + # ignore __pycache__ + if name != "__pycache__": + subdir_name = os.path.join(*names_so_far, name) + if os.path.isdir(subdir_name): + new_name_list = names_so_far + [name] + for filename in os.listdir(os.path.join(*new_name_list)): + base_file, file_extension = os.path.splitext(filename) + # if it's a .py + if file_extension == ".py": + new_name = ".".join(new_name_list+[base_file]) + if not new_name in returnvalue: + returnvalue.append(new_name) + returnvalue.extend(recurse_for_py_files(new_name_list)) + return returnvalue + + +hiddenimports = recurse_for_py_files(["source"]) +for hidden in (collect_submodules("pkg_resources")): + hiddenimports.append(hidden) + +a = Analysis( + [f"../{BINARY_SLUG}.py"], + pathex=[], + binaries=[], + datas=[('../data/', 'data/')], + hiddenimports=hiddenimports, + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False +) + +# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller +excluded_binaries = [ + 'mfc140u.dll', + 'msvcp140.dll', + 'ucrtbase.dll', + 'VCRUNTIME140.dll' +] + +# win is temperamental +with open(os.path.join(".","resources","app","meta","manifests","excluded_dlls.json")) as dllsManifest: + dlls = [] + try: + dlls = json.load(dllsManifest) + except JSONDecodeError as e: + raise ValueError("Windows DLLs manifest malformed!") + for dll in dlls: + for submod in ["core", "crt"]: + for ver in ["1-1-0", "1-1-1", "1-2-0", "2-1-0"]: + excluded_binaries.append(f"api-ms-win-{submod}-{dll}-l{ver}.dll") + +a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) + +pyz = PYZ( + a.pure, + a.zipped_data, + cipher=block_cipher +) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name=BINARY_SLUG, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=console +) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index c67d760f..33fd8f87 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -138,6 +138,7 @@ class CustomSettings(object): args.experimental[p] = get_setting(settings['experimental'], args.experimental[p]) args.collection_rate[p] = get_setting(settings['collection_rate'], args.collection_rate[p]) args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p]) + args.prizeshuffle[p] = get_setting(settings['prizeshuffle'], args.prizeshuffle[p]) args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p]) args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p]) args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p]) @@ -320,6 +321,7 @@ class CustomSettings(object): settings_dict[p]['experimental'] = world.experimental[p] settings_dict[p]['collection_rate'] = world.collection_rate[p] settings_dict[p]['openpyramid'] = world.open_pyramid[p] + settings_dict[p]['prizeshuffle'] = world.prizeshuffle[p] settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p] settings_dict[p]['keyshuffle'] = world.keyshuffle[p] settings_dict[p]['mapshuffle'] = world.mapshuffle[p] diff --git a/source/classes/SFX.py b/source/classes/SFX.py index 1cc85f16..b98a76f8 100644 --- a/source/classes/SFX.py +++ b/source/classes/SFX.py @@ -473,8 +473,8 @@ sfx_instrument_changes = [ SFXInstrumentChange(0x02, 0x18, 0x10, [0x1A9707], ban=[0x00, 0x01, 0x06, 0x0C, 0x18]), SFXInstrumentChange(0x02, 0x19, 0x10, [0x1A971F], ban=[0x00, 0x01, 0x06, 0x0C, 0x18]), SFXInstrumentChange(0x02, 0x1A, 0x01, [0x1A96C7], type=Am, ban=[0x00, 0x06, 0x13], inc=[0x08, 0x0F, 0x10, 0x12, 0x15, 0x17]), - SFXInstrumentChange(0x02, 0x1B, 0x11, [0x1A96B8], type=Am, ban=[0x00, 0x06, 0x13], inc=[0x08, 0x0F, 0x10, 0x12, 0x15, 0x17]), - SFXInstrumentChange(0x02, 0x1C, 0x11, [0x1A96B2], type=Am, ban=[0x00, 0x06, 0x13], inc=[0x08, 0x0F, 0x10, 0x12, 0x15, 0x17]), + SFXInstrumentChange(0x02, 0x1B, 0x11, [0x1A96B8], type=Am, ban=[0x00, 0x01, 0x06, 0x13], inc=[0x08, 0x0F, 0x10, 0x12, 0x15, 0x17]), + SFXInstrumentChange(0x02, 0x1C, 0x11, [0x1A96B2], type=Am, ban=[0x00, 0x01, 0x06, 0x13], inc=[0x08, 0x0F, 0x10, 0x12, 0x15, 0x17]), SFXInstrumentChange(0x02, 0x1D, 0x16, [0x1A966C], ban=[0x09, 0x10, 0x11, 0x13, 0x15, 0x18]), SFXInstrumentChange(0x02, 0x1E, 0x01, [0x1A9928], type=Lg, ban=[0x06]), SFXInstrumentChange(0x02, 0x1F, 0x02, [0x1A969A], type=Me|Be|Hd, ban=[0x09]), diff --git a/source/classes/appversion.py b/source/classes/appversion.py new file mode 100644 index 00000000..3d677d1e --- /dev/null +++ b/source/classes/appversion.py @@ -0,0 +1,17 @@ +import os + +from OverworldShuffle import __version__ +OWR_VERSION = __version__ + +def write_appversion(): + APP_VERSION = OWR_VERSION + if "-" in APP_VERSION: + APP_VERSION = APP_VERSION[:APP_VERSION.find("-")] + APP_VERSION_FILE = os.path.join(".","resources","app","meta","manifests","app_version.txt") + with open(APP_VERSION_FILE,"w") as f: + f.seek(0) + f.truncate() + f.write(APP_VERSION) + +if __name__ == "__main__": + write_appversion() diff --git a/source/classes/constants.py b/source/classes/constants.py index 1f62560c..4d170746 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -111,6 +111,7 @@ SETTINGSTOPROCESS = { "mapshuffle": "mapshuffle", "compassshuffle": "compassshuffle", "bigkeyshuffle": "bigkeyshuffle", + "prizeshuffle": "prizeshuffle", "key_logic_algorithm": "key_logic_algorithm", "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", @@ -146,7 +147,7 @@ SETTINGSTOPROCESS = { }, "generation": { "bps": "bps", - "createspoiler": "create_spoiler", + "spoiler": "spoiler", "createrom": "create_rom", "calcplaythrough": "calc_playthrough", "print_custom_yaml": "print_custom_yaml", diff --git a/source/classes/diags.py b/source/classes/diags.py index 3e2c4121..e26c4e43 100644 --- a/source/classes/diags.py +++ b/source/classes/diags.py @@ -1,16 +1,28 @@ import platform, sys, os, subprocess -import pkg_resources -from datetime import datetime +try: + import pkg_resources +except ModuleNotFoundError as e: + pass +import datetime + +from Main import __version__ +DR_VERSION = __version__ + +from OverworldShuffle import __version__ +OWR_VERSION = __version__ + +PROJECT_NAME = "ALttP Overworld Randomizer" def diagpad(str): - return str.ljust(len("ALttP Door Randomizer Version") + 5,'.') + return str.ljust(len(f"{PROJECT_NAME} Version") + 5,'.') -def output(APP_VERSION): +def output(): lines = [ - "ALttP Door Randomizer Diagnostics", + f"{PROJECT_NAME} Diagnostics", "=================================", - diagpad("UTC Time") + str(datetime.utcnow())[:19], - diagpad("ALttP Door Randomizer Version") + APP_VERSION, + diagpad("UTC Time") + str(datetime.datetime.now(datetime.UTC))[:19], + diagpad("ALttP Door Randomizer Version") + DR_VERSION, + diagpad(f"{PROJECT_NAME} Version") + OWR_VERSION, diagpad("Python Version") + platform.python_version() ] lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release())) @@ -35,6 +47,7 @@ def output(APP_VERSION): pkg = pkg.split("==") lines.append(diagpad(pkg[0]) + pkg[1]) ''' + installed_packages = [] installed_packages = [str(d) for d in pkg_resources.working_set] #this doesn't work from the .exe either, but it doesn't crash the program installed_packages.sort() for pkg in installed_packages: diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 84139157..7f0efec5 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -5,7 +5,7 @@ import time from BaseClasses import CrystalBarrier, DoorType, Hook, RegionType, Sector from BaseClasses import hook_from_door, flooded_keys -from Regions import dungeon_events, flooded_keys_reverse +from Regions import location_events, flooded_keys_reverse def pre_validate(builder, entrance_region_names, split_dungeon, world, player): @@ -556,7 +556,7 @@ class ExplorationState(object): if key_checks and location not in self.found_locations: if location.forced_item and 'Small Key' in location.item.name: self.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: + if location.name not in location_events and not ('- Prize' in location.name and location.prize) and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) @@ -568,13 +568,13 @@ class ExplorationState(object): else: self.bk_found.add(location) self.re_add_big_key_doors() - if location.name in dungeon_events and location.name not in self.events: + if location.name in location_events and location.name not in self.events: if self.flooded_key_check(location): self.perform_event(location.name, key_region) if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) - if '- Prize' in location.name: + if location.prize: self.prize_received = True def flooded_key_check(self, location): @@ -837,7 +837,7 @@ def count_locations_exclude_big_chest(locations, world, player): def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + return loc.name in location_events or loc.prize def reserved_location(loc, world, player): diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index fc07cb1c..c6027ef0 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -15,6 +15,7 @@ from Items import ItemFactory from PotShuffle import key_drop_special from Utils import snes_to_pc, pc_to_snes, int16_as_bytes +from source.overworld.EntranceData import door_addresses class EnemyStats: def __init__(self, sprite, static, drop_flag=False, prize_pack: typing.Union[tuple, int] = 0, @@ -2232,7 +2233,6 @@ def setup_enemy_dungeon_tables(world, player): def find_entrance_ids(region): - from EntranceShuffle import door_addresses entrance_list = [] queue = deque([region]) visited = {region} diff --git a/source/enemizer/Enemizer.py b/source/enemizer/Enemizer.py index 65009fdd..021b7f03 100644 --- a/source/enemizer/Enemizer.py +++ b/source/enemizer/Enemizer.py @@ -260,7 +260,7 @@ def get_randomize_able_sprites_ow(area_id, data_tables): req = data_tables.sprite_requirements[key] if isinstance(req, dict): continue - if not req.static and req.can_randomize: + if not req.static and req.can_randomize and not sprite.static: sprite_table[idx] = sprite return sprite_table diff --git a/source/enemizer/OwEnemyList.py b/source/enemizer/OwEnemyList.py index 58c15b43..f89b16c6 100644 --- a/source/enemizer/OwEnemyList.py +++ b/source/enemizer/OwEnemyList.py @@ -29,6 +29,7 @@ def init_vanilla_sprites_ow(): create_sprite(0x21b, EnemySprite.GreenKnifeGuard, 0x20, 0x1A, '', 0x09CB51) create_sprite(0x21b, EnemySprite.TutorialGuard, 0x2D, 0x25, '', 0x09CB54) create_sprite(0x21b, EnemySprite.TutorialGuard, 0x20, 0x29, '', 0x09CB57) + create_sprite(0x21b, EnemySprite.TutorialGuard, 0x3C, 0x2A, '', 0x09CB5B) create_sprite(0x21d, EnemySprite.Apple, 0x0B, 0x06, '', 0x09CB5B, bonk=True) create_sprite(0x22b, EnemySprite.TutorialGuard, 0x09, 0x1E, '', 0x09CB5F) create_sprite(0x22b, EnemySprite.TutorialGuard, 0x0B, 0x1E, '', 0x09CB62) diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 2a935048..108ac0b3 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -118,48 +118,48 @@ LenientTrapsForTesting = {0x16, 0x26, 0x3f, 0x40, 0x42, 0x46, 0x49, 0x4e, 0x57, 0x65, 0x6a, 0x74, 0x76, 0x7d, 0x98, 0x9e, 0xaf, 0xba, 0xc6, 0xcb, 0xce, 0xd2, 0xd5, 0xd8, 0xdf, 0xe4, 0xe7, 0xee, 0xfd, 0x10c} -# this will have to be dynamic if cave rooms are allowed in dungeons -WallmasterValidRooms = { - HC_NorthCorridor, HC_SwitchRoom, HoulihanRoom, TR_CrystalRollerRoom, - PalaceofDarkness0x09, PoD_StalfosTrapRoom, PoD_TurtleRoom, GT_EntranceRoom, Ice_EntranceRoom, - GanonEvacuationRoute, HC_BombableStockRoom, Sanctuary, TR_Hokku_BokkuKeyRoom2, TR_BigKeyRoom, TurtleRock0x15, - Swamp_SwimmingTreadmill, Hera_MoldormFallRoom, PoD_DarkMaze, PoD_BigChestRoom, PoD_Mimics_MovingWallRoom, - GT_IceArmos, GT_FinalHallway, Ice_BombFloor_BariRoom, Ice_Pengator_BigKeyRoom, Tower_Agahnim, HC_KeyRatRoom, - HC_SewerTextTriggerRoom, TR_WestExittoBalcony, TR_DoubleHokku_Bokku_BigchestRoom, Swamp_StatueRoom, Hera_BigChest, - Swamp_EntranceRoom, Skull_Mothula, PoD_BigHubRoom, PoD_MapChest_FairyRoom, Ice_CompassRoom, Hera_HardhatBeetlesRoom, - HC_SewerKeyChestRoom, Desert_Lanmolas, Swamp_PushBlockPuzzle_Pre_BigKeyRoom, Swamp_BigKey_BSRoom, - Swamp_BigChestRoom, Swamp_MapChest_WaterFillRoom, Swamp_KeyPotRoom, Skull_GibdoKey_MothulaHoleRoom, - PoD_BombableFloorRoom, PoD_SpikeBlock_ConveyorRoom, GT_TorchRoom2, Ice_StalfosKnights_ConveyorHellway, - Ice_MapChestRoom, Tower_FinalBridgeRoom, HC_FirstDarkRoom, HC_6RopesRoom, Desert_TorchPuzzle_MovingWallRoom, - TT_BigChestRoom, TT_JailCellsRoom, Swamp_CompassChestRoom, Skull_GibdoTorchPuzzleRoom, PoD_EntranceRoom, - PoD_Warps_SouthMimicsRoom, GT_Mini_HelmasaurConveyorRoom, GT_MoldormRoom, Ice_Bomb_JumpRoom, - IcePalaceCloneRoom_FairyRoom, HC_WestCorridor, HC_ThroneRoom, HC_EastCorridor, Desert_Popos2_BeamosHellwayRoom, - Swamp_UpstairsPitsRoom, CastleSecretEntrance_UncleDeathRoom, Skull_KeyPot_TrapRoom, Skull_BigKeyRoom, - Skull_BigChestRoom, Skull_FinalSectionEntranceRoom, PoD_HelmasaurKing, GT_SpikePitRoom, GT_Ganon_BallZ, - GT_Gauntlet1_2_3, Ice_LonelyFirebar, Ice_HiddenChest_SpikeFloorRoom, HC_WestEntranceRoom, HC_MainEntranceRoom, - HC_EastEntranceRoom, Desert_FinalSectionEntranceRoom, TT_WestAtticRoom, TT_EastAtticRoom, - Swamp_HiddenChest_HiddenDoorRoom, Skull_CompassChestRoom, Skull_KeyChest_TrapRoom, PoD_RupeeRoom, GT_MimicsRooms, - GT_LanmolasRoom, GT_Gauntlet4_5, Ice_PengatorsRoom, HC_SmallCorridortoJailCells, HC_BoomerangChestRoom, - HC_MapChestRoom, Desert_BigChestRoom, Desert_MapChestRoom, Desert_BigKeyChestRoom, Swamp_WaterDrainRoom, - Hera_EntranceRoom, GanonsTower, GT_EastSideCollapsingBridge_ExplodingWallRoom, GT_Winder_WarpMazeRoom, - Ice_HiddenChest_BombableFloorRoom, Ice_BigSpikeTrapsRoom, HC_JailCellRoom, HC_NextToChasmRoom, HC_BasementChasmRoom, - Desert_WestEntranceRoom, Desert_MainEntranceRoom, Desert_EastEntranceRoom, Hera_TileRoom, Eastern_FairyRoom, - GT_BlockPuzzle_SpikeSkip_MapChestRoom, GT_EastandWestDownstairs_BigChestRoom, GT_Tile_TorchPuzzleRoom, - IcePalace0x8E, Mire_Vitreous, Mire_FinalSwitchRoom, Mire_DarkBombWall_SwitchesRoom, - Mire_DarkCaneFloorSwitchPuzzleRoom, GT_FinalCollapsingBridgeRoom, GT_Torches1Room, Mire_TorchPuzzle_MovingWallRoom, - Mire_EntranceRoom, Eastern_EyegoreKeyRoom, GT_ManySpikes_WarpMazeRoom, GT_InvisibleFloorMazeRoom, - GT_CompassChest_InvisibleFloorRoom, Ice_BigChestRoom, IcePalace0x9F, Mire_Pre_VitreousRoom, Mire_FishRoom, - Mire_BridgeKeyChestRoom, MiseryMire0xA3, TR_Trinexx, GT_WizzrobesRooms, GT_MoldormFallRoom, Hera_FairyRoom, - Eastern_StalfosSpawnRoom, Eastern_BigChestRoom, Eastern_MapChestRoom, TT_MovingSpikes_KeyPotRoom, TT_BlindTheThief, - IcePalace0xAE, Ice_IceBridgeRoom, Tower_CircleofPots, Mire_HourglassRoom, Mire_SlugRoom, Mire_SpikeKeyChestRoom, - TR_Pre_TrinexxRoom, TR_DarkMaze, TR_ChainChompsRoom, TR_MapChest_KeyChest_RollerRoom, Eastern_BigKeyRoom, - Eastern_LobbyCannonballsRoom, Eastern_DarkAntifairy_KeyPotRoom, TT_Hellway, TT_ConveyorToilet, Ice_BlockPuzzleRoom, - IcePalaceCloneRoom_SwitchRoom, Tower_DarkBridgeRoom, Mire_CompassChest_TileRoom, Mire_BigHubRoom, Mire_BigChestRoom, - TR_FinalCrystalSwitchPuzzleRoom, TR_LaserBridge, TurtleRock0xC6, TR_TorchPuzzle, - Eastern_EntranceRoom, UnknownRoom, TT_NorthWestEntranceRoom, TT_NorthEastEntranceRoom, Ice_HoletoKholdstareRoom, - Tower_DarkMaze, Mire_ConveyorSlug_BigKeyRoom, Mire_Mire02_WizzrobesRoom, TR_LaserKeyRoom, TR_EntranceRoom, - Eastern_PreArmosKnightsRoom, Eastern_CanonballRoom, EasternPalace, TT_Main_SouthWestEntranceRoom, - TT_SouthEastEntranceRoom, Tower_EntranceRoom + +# wallmasters must not be on tiles near spiral staircases. Unknown if other stairs have issues +WallmasterInvalidRooms = { + HC_NorthCorridor, HC_SwitchRoom, TR_CrystalRollerRoom, + PalaceofDarkness0x09, PoD_StalfosTrapRoom, GT_EntranceRoom, Ice_EntranceRoom, + HC_BombableStockRoom, TurtleRock0x15, + Swamp_SwimmingTreadmill, Hera_MoldormFallRoom, PoD_BigChestRoom, + GT_IceArmos, GT_FinalHallway, Ice_BombFloor_BariRoom, + Swamp_StatueRoom, Hera_BigChest, + Swamp_EntranceRoom, Hera_HardhatBeetlesRoom, + Swamp_PushBlockPuzzle_Pre_BigKeyRoom, + Swamp_KeyPotRoom, + PoD_BombableFloorRoom, + Ice_MapChestRoom, Tower_FinalBridgeRoom, HC_FirstDarkRoom, HC_6RopesRoom, + TT_JailCellsRoom, PoD_EntranceRoom, + GT_Mini_HelmasaurConveyorRoom, GT_MoldormRoom, Ice_Bomb_JumpRoom, + Desert_Popos2_BeamosHellwayRoom, + GT_Ganon_BallZ, + GT_Gauntlet1_2_3, Ice_HiddenChest_SpikeFloorRoom, + Desert_FinalSectionEntranceRoom, TT_WestAtticRoom, + Swamp_HiddenChest_HiddenDoorRoom, PoD_RupeeRoom, GT_MimicsRooms, + GT_LanmolasRoom, Ice_PengatorsRoom, HC_SmallCorridortoJailCells, HC_BoomerangChestRoom, + HC_MapChestRoom, Swamp_WaterDrainRoom, + Hera_EntranceRoom, + Ice_BigSpikeTrapsRoom, HC_JailCellRoom, + Hera_TileRoom, + GT_EastandWestDownstairs_BigChestRoom, + IcePalace0x8E, Mire_FinalSwitchRoom, + Mire_DarkCaneFloorSwitchPuzzleRoom, Mire_TorchPuzzle_MovingWallRoom, + Mire_EntranceRoom, Eastern_EyegoreKeyRoom, + Ice_BigChestRoom, Mire_Pre_VitreousRoom, + Mire_BridgeKeyChestRoom, GT_WizzrobesRooms, GT_MoldormFallRoom, + TT_MovingSpikes_KeyPotRoom, + IcePalace0xAE, Tower_CircleofPots, + TR_DarkMaze, TR_ChainChompsRoom, + TT_ConveyorToilet, Ice_BlockPuzzleRoom, + Tower_DarkBridgeRoom, + UnknownRoom, + Tower_DarkMaze, Mire_ConveyorSlug_BigKeyRoom, Mire_Mire02_WizzrobesRoom, + EasternPalace, + Tower_EntranceRoom, Cave_BackwardsDeathMountainTopFloor, Cave0xE8, Cave_SpectacleRockHP, Cave0xEB, Cave0xED, + Cave_CrystalSwitch_5ChestsRoom, Cave0xF8, Cave0xFA, Cave0xFB, Cave0xFD, Cave0xFF } @@ -293,7 +293,7 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.Terrorpin).sub_group(2, 0x2a).exclude({0x10c}), # probably fine in mimic now SpriteRequirement(EnemySprite.Blob).sub_group(1, 0x20), SpriteRequirement(EnemySprite.Wallmaster).immune().ow_skip().sub_group(2, 0x23) - .allow(WallmasterValidRooms), + .exclude(WallmasterInvalidRooms), SpriteRequirement(EnemySprite.StalfosKnight).sub_group(1, 0x20).exclude({0x10c}), SpriteRequirement(EnemySprite.HelmasaurKing).exalt().sub_group(2, 0x3a).sub_group(3, 0x3e), SpriteRequirement(EnemySprite.Bumper).immune().aquaphobia().sub_group(3, [0x52, 0x53]), @@ -687,7 +687,11 @@ def setup_required_overworld_groups(sheets): sheets[4].add_sprite_to_sheet([None, None, None, None], {0xF, 0x9F}) # Waterfall of wishing (pre/post-Aga) sheets[3].add_sprite_to_sheet([None, None, None, 14], {0x14, 0xA4}) # Graveyard (pre/post-Aga) sheets[1].add_sprite_to_sheet([None, None, 76, 0x3F], {0x1B, 0xAB}) # Hyrule Castle (pre/post-Aga) - sheets[2].add_sprite_to_sheet([None, None, None, 0x3F], {}) # Hyrule Castle - rain state + ## group 0 set to 0x48 for tutortial guards + ## group 1 & 2 set for green knife guards (and probably normal green guard) + ## group 3 set for lightning gate + sheets[2].add_sprite_to_sheet([0x48, 0x49, 0x13, 0x3F], {}) # Hyrule Castle - rain state + # Smithy/Race/Kak (pre/post-Aga) sheets[6].add_sprite_to_sheet([0x4F, 0x49, 0x4A, 0x50], {0x18, 0x22, 0x28, 0xA8, 0xB2, 0xB8}) sheets[8].add_sprite_to_sheet([None, None, 18, None], {0x30, 0xC0}) # Desert (pre/post-Aga) diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index d6dc6564..65a96ab1 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -109,7 +109,7 @@ UwGeneralDeny: - [ 0x0044, 6, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "BigSpike" ] ] #"Thieves' Town - Joke Room - Red Bari" - [ 0x0044, 8, [ "Statue", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Joke Room - Blue Bari 4" - [ 0x0045, 1, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Thieves' Town - Basement Block Totems - Red Zazak" - - [ 0x0045, 4, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots + - [ 0x0045, 4, [ "Wizzrobe", "Statue", "Lynel" ] ] # Wizzrobes can't spawn on pots - [ 0x0045, 7, [ "AntiFairyCircle", "Bumper" ] ] #"Thieves' Town - Cells - Blue Zazak 4" - [ 0x0045, 8, [ "RollerHorizontalRight" ] ] #"Thieves' Town - Cells - Zol" - [ 0x0046, 0, [ "RollerVerticalUp", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper", "Statue" ] ] #"Swamp Palace - Big O Top - Hover 1" @@ -166,7 +166,7 @@ UwGeneralDeny: - [ 0x005e, 4, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Ice Palace - Pit Trap - Fire Bar (Clockwise)" - [ 0x005f, 0, [ "RollerVerticalDown", "RollerHorizontalLeft" ] ] #"Ice Palace - Bari University - Blue Bari 1" - [ 0x005f, 1, [ "RollerVerticalDown", "RollerHorizontalRight" ] ] #"Ice Palace - Bari University - Blue Bari 2" - - [ 0x0060, 0, [ "RollerVerticalUp", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper", "Beamos" ] ] #"Hyrule Castle - West - Blue Guard" + - [ 0x0060, 0, [ "RollerVerticalUp", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper", "Beamos", "SpikeBlock" ] ] #"Hyrule Castle - West - Blue Guard" - [ 0x0062, 0, [ "RollerVerticalUp", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Hyrule Castle - East - Blue Guard" - [ 0x0064, 2, [ "Bumper" , "Beamos" ] ] #"Thieves' Town - Attic Hall Left - Keese 2" - [ 0x0064, 3, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots @@ -325,6 +325,7 @@ UwGeneralDeny: - [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur" - [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4" + - [0x00c6, 5, ["Bumper"]] - [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1" - [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2" @@ -419,10 +420,12 @@ OwGeneralDeny: - [0x03, 10, ["Gibo"]] # OldMan eating Gibo - [0x05, 11, ["Bumper", "AntiFairyCircle"]] # Blocks path to portal - [0x1e, 3, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] # forbid a beamos here + - [0x35, 8, ["RollerVerticalUp", "RollerVerticalDown"]] # blocks the dock - [0x40, 0, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 7, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 13, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 14, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] + - [0x40, 16, ["RollerVerticalUp", "RollerVerticalDown"]] # Ropa near back hole is really large as a roller - [0x55, 6, ["BigSpike"]] - [0x57, 5, ["RollerVerticalUp", "RollerVerticalDown"]] - [0x5e, 0, ["Gibo"]] # kiki eating Gibo diff --git a/source/gui/bottom.py b/source/gui/bottom.py index 1c8f838a..1cd99e0a 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -133,7 +133,6 @@ def bottom_frame(self, parent, args=None): made[k] = m.group(1) + m.group(2) + ' ' + m.group(4) successMsg += (made["rom"] % (YES if (guiargs.create_rom) else NO)) + "\n" successMsg += (made["playthrough"] % (YES if (guiargs.calc_playthrough) else NO)) + "\n" - successMsg += (made["spoiler"] % (YES if (not guiargs.jsonout and guiargs.create_spoiler) else NO)) + "\n" successMsg += (made["enemizer"] % (YES if needEnemizer else NO)) + "\n" # FIXME: English successMsg += ("Seed%s: %s" % ('s' if len(seeds) > 1 else "", ','.join(str(x) for x in seeds))) diff --git a/source/gui/randomize/generation.py b/source/gui/randomize/generation.py index c66eceb6..868402e4 100644 --- a/source/gui/randomize/generation.py +++ b/source/gui/randomize/generation.py @@ -154,7 +154,7 @@ def generation_page(parent,settings): diag.geometry(str(dims["window"]["width"]) + 'x' + str(dims["window"]["height"])) text = Text(diag, width=dims["textarea.characters"]["width"], height=dims["textarea.characters"]["height"]) text.pack() - text.insert(INSERT,"\n".join(diagnostics.output(__version__))) + text.insert(INSERT,"\n".join(diagnostics.output())) # dialog button self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"], text='Run Diagnostics', command=partial(diags)) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 1e8a8092..650287c4 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -59,6 +59,8 @@ def create_item_pool_config(world): if info.prize: d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') + if world.prizeshuffle[player] != 'none': + config.reserved_locations[player].add(f'{d_name} - Prize') for dungeon in world.dungeons: if world.restrict_boss_items[dungeon.player] != 'none': for item in dungeon.all_items: @@ -118,6 +120,9 @@ def create_item_pool_config(world): LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: config.reserved_locations[player].add(loc_name) + if world.prizeshuffle[player] != 'none': + for loc_name in mode_grouping['Prizes']: + config.reserved_locations[player].add(loc_name) elif world.algorithm == 'major_only': config.location_groups = [ LocationGroup('MajorItems'), @@ -127,6 +132,8 @@ def create_item_pool_config(world): init_set = mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers'] for player in range(1, world.players + 1): groups = LocationGroup('Major').locs(init_set) + if world.prizeshuffle[player] != 'none': + groups.locations.extend(mode_grouping['Prizes']) if world.bigkeyshuffle[player]: groups.locations.extend(mode_grouping['Big Keys']) if world.dropshuffle[player] != 'none': @@ -251,21 +258,51 @@ def location_prefilled(location, world, player): def previously_reserved(location, world, player): - if '- Boss' in location.name: + if '- Boss' in location.name or '- Prize' in location.name: if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] or not world.mapshuffle[player]): return True if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] or not world.mapshuffle[player] or not world.bigkeyshuffle[player] - or world.keyshuffle[player] == 'none'): + or world.keyshuffle[player] == 'none' + or world.prizeshuffle[player] in ['none', 'dungeon']): return True return False +def verify_item_pool_config(world): + if world.algorithm == 'major_only': + major_pool = defaultdict(list) + for item in world.itempool: + if item.name in world.item_pool_config.item_pool[item.player]: + major_pool[item.player].append(item) + for player in major_pool: + available_locations = [world.get_location(l, player) for l in world.item_pool_config.location_groups[0].locations] + available_locations = [l for l in available_locations if l.item is None] + if len(available_locations) < len(major_pool[player]): + if len(major_pool[player]) - len(available_locations) <= len(mode_grouping['Heart Pieces Visible']): + logging.getLogger('').warning('Expanding location pool for extra major items') + world.item_pool_config.location_groups[1].locations = set(mode_grouping['Heart Pieces Visible']) + else: + raise Exception(f'Major only: there are only {len(available_locations)} locations' + f' for {len(major_pool[player])} major items for player {player}. Cannot generate.') + + def massage_item_pool(world): player_pool = defaultdict(list) + dungeon_pool = defaultdict(list) + for dungeon in world.dungeons: + if dungeon_table[dungeon.name].prize: + dungeon_pool[dungeon.player].append(dungeon) + for player in dungeon_pool: + dungeons = list(dungeon_pool[player]) + random.shuffle(dungeons) + dungeon_pool[player] = dungeons for item in world.itempool: + if item.prize: + dungeon = dungeon_pool[item.player].pop() + dungeon.prize = item player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: @@ -273,7 +310,7 @@ def massage_item_pool(world): player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: - player_locations[player] = [x for x in world.get_unfilled_locations(player) if '- Prize' not in x.name] + player_locations[player] = [x for x in world.get_unfilled_locations(player) if not x.prize] discrepancy = len(player_pool[player]) - len(player_locations[player]) if discrepancy: trash_options = [x for x in player_pool[player] if x.name in trash_items] @@ -342,6 +379,8 @@ def determine_major_items(world, player): major_item_set = set(major_items) if world.progressive == 'off': pass # now what? + if world.prizeshuffle[player] not in ['none', 'dungeon']: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Prize'}) if world.bigkeyshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player] != 'none': @@ -413,6 +452,9 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion if item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations filtered = [l for l in locations if l.name in restricted] + if len(filtered) == 0 and len(config.location_groups[1].locations) > 0: + restricted = config.location_groups[1].locations + filtered = [l for l in locations if l.name in restricted] return filtered if world.algorithm == 'district': config = world.item_pool_config @@ -687,6 +729,17 @@ mode_grouping = { 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", 'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic Cave' ], + 'Prizes': [ + 'Eastern Palace - Prize', 'Desert Palace - Prize', 'Tower of Hera - Prize', + 'Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Skull Woods - Prize', + "Thieves' Town - Prize", 'Ice Palace - Prize', 'Misery Mire - Prize', 'Turtle Rock - Prize' + ], + 'Heart Pieces Visible': [ + 'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island', # visible on OW + 'Maze Race', 'Pyramid', "Zora's Ledge", 'Sunken Treasure', 'Spectacle Rock', + 'Lumberjack Tree', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', + 'Peg Cave', 'Cave 45', 'Graveyard Cave' + ], 'Big Keys': [ 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest', @@ -758,7 +811,7 @@ mode_grouping = { 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', 'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', - 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Mini Helmasuar Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Mini Helmasaur Key Drop', ], 'Pot Keys': [ 'Eastern Palace - Dark Square Pot Key', 'Desert Palace - Desert Tiles 1 Pot Key', diff --git a/source/meta/build-dr.py b/source/meta/build-dr.py deleted file mode 100644 index a83c9d56..00000000 --- a/source/meta/build-dr.py +++ /dev/null @@ -1,27 +0,0 @@ -import subprocess -import os -import shutil -import sys - -# Spec file -SPEC_FILE = os.path.join(".", "source", "DungeonRandomizer.spec") - -# Destination is current dir -DEST_DIRECTORY = '.' - -# Check for UPX -if os.path.isdir("upx"): - upx_string = "--upx-dir=upx" -else: - upx_string = "" - -if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.find("osx"): - shutil.rmtree("build") - -# Run pyinstaller for DungeonRandomizer -subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", - upx_string, - "-y ", - f"--distpath {DEST_DIRECTORY} ", - ]), - shell=True) diff --git a/source/meta/build-gui.py b/source/meta/build-gui.py deleted file mode 100644 index 4986df67..00000000 --- a/source/meta/build-gui.py +++ /dev/null @@ -1,27 +0,0 @@ -import subprocess -import os -import shutil -import sys - -# Spec file -SPEC_FILE = os.path.join(".", "source", "Gui.spec") - -# Destination is current dir -DEST_DIRECTORY = '.' - -# Check for UPX -if os.path.isdir("upx"): - upx_string = "--upx-dir=upx" -else: - upx_string = "" - -if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.find("osx"): - shutil.rmtree("build") - -# Run pyinstaller for Gui -subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", - upx_string, - "-y ", - f"--distpath {DEST_DIRECTORY} ", - ]), - shell=True) diff --git a/source/meta/build.py b/source/meta/build.py new file mode 100644 index 00000000..4cc29436 --- /dev/null +++ b/source/meta/build.py @@ -0,0 +1,155 @@ +''' +Build Entrypoints +''' +import json +import platform +import os # for checking for dirs +import re +from json.decoder import JSONDecodeError +from subprocess import Popen, PIPE, STDOUT, CalledProcessError + +DEST_DIRECTORY = "." + +# UPX greatly reduces the filesize. You can get this utility from https://upx.github.io/ +# just place it in a subdirectory named "upx" and this script will find it +UPX_DIR = "upx" +if os.path.isdir(os.path.join(".", UPX_DIR)): + upx_string = f"--upx-dir={UPX_DIR}" +else: + upx_string = "" +GO = True +DIFF_DLLS = False + +# set a global var for Actions to try to read +def set_output(name, value): + with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: + print(f'{name}={value}', file=fh) + +# build the thing +def run_build(slug): + global GO + global DIFF_DLLS + + print(f"Building '{slug}' via Python {platform.python_version()}") + + # get template, mod to do the thing + specTemplateFile = open(os.path.join(".","source","Template.spec")) + specTemplate = specTemplateFile.read() + specTemplateFile.close() + with(open(os.path.join(".","source",f"{slug}.spec"), "w")) as specFile: + print(f"Writing '{slug}' PyInstaller spec file") + thisTemplate = specTemplate.replace("", slug) + specFile.write(thisTemplate) + + PYINST_EXECUTABLE = "pyinstaller" + args = [ + os.path.join("source", f"{slug}.spec").replace(os.sep, os.sep * 2), + upx_string, + "-y", + f"--distpath={DEST_DIRECTORY}" + ] + errs = [] + strs = [] + print("PyInstaller args: %s" % " ".join(args)) + cmd = [ + PYINST_EXECUTABLE, + *args + ] + + ret = { + "stdout": [], + "stderr": [] + } + + with Popen(cmd, stdout=PIPE, stderr=STDOUT, bufsize=1, universal_newlines=True) as p: + for line in p.stdout: + ret["stdout"].append(line) + print(line, end='') + # if p.stderr: + # for line in p.stderr: + # ret["stderr"].append(line) + # print(line, end='') + # if p.returncode != 0: + # raise CalledProcessError(p.returncode, p.args) + + # check stdout & stderr + for key in ["stdout","stderr"]: + if len(ret[key]) > 0: + for line in ret[key]: + # UPX can't compress this file + if "NotCompressibleException" in line.strip(): + print(line) + errs.append(line.strip()) + # print UPX messages + if "UPX" in line: + print(line) + # try to get DLL filename + elif "NotCompressibleException" in line.strip(): + matches = re.search(r'api-ms-win-(?:[^-]*)-([^-]*)', line.strip()) + if matches: + strAdd = matches.group(1) + strs.append(strAdd) + errs.append(line.strip()) + # print collected errors + if len(errs) > 0: + print("=" * 10) + print("| ERRORS |") + print("=" * 10) + print("\n".join(errs)) + else: + GO = False + + # if we identified DLLs to ignore + if len(strs) > 0: + # read DLLs manifest that we've already got saved + with open(os.path.join(".","resources","app","meta","manifests","excluded_dlls.json"), "w+", encoding="utf-8") as dllsManifest: + oldDLLs = [] + try: + oldDLLs = json.load(dllsManifest) + except JSONDecodeError as e: + oldDLLs = [] + # raise ValueError("Windows DLLs manifest malformed!") + + # bucket for new list + newDLLs = sorted(list(set(oldDLLs))) + + # items to add + addDLLs = sorted(list(set(strs))) + + # add items + newDLLs += addDLLs + newDLLs = sorted(list(set(newDLLs))) + + # if the lists differ, we've gotta update the included list + diffDLLs = newDLLs != oldDLLs + + if diffDLLs: + DIFF_DLLS = True + dllsManifest.seek(0) + dllsManifest.truncate() + dllsManifest.write(json.dumps(sorted(newDLLs), indent=2)) + + print(f"Old DLLs: {json.dumps(sorted(oldDLLs))}") + print(f"Add DLLs: {json.dumps(sorted(addDLLs))}") + print(f"New DLLs: {json.dumps(sorted(newDLLs))}") + print(f"Diff DLLs: {DIFF_DLLS}") + print("") + +def go_build(slug): + slug = slug or "" + if slug != "": + GO = True + while GO: + run_build(slug) + GO = False + +if __name__ == "__main__": + binary_slugs = [] + #TODO: Make sure we've got the proper binaries that we need + with open(os.path.join(".","resources","app","meta","manifests","binaries.json")) as binariesFile: + binary_slugs = json.load(binariesFile) + for file_slug in binary_slugs: + go_build(file_slug) + if DIFF_DLLS: + print("🔴Had to update Error DLLs list!") + exit(1) diff --git a/source/meta/check_errordlls.py b/source/meta/check_errordlls.py new file mode 100644 index 00000000..b9a56590 --- /dev/null +++ b/source/meta/check_errordlls.py @@ -0,0 +1,10 @@ +import json +import os + +error_dlls_path = os.path.join(".","resources","app","meta","manifests","excluded_dlls.json") +if os.path.isfile(error_dlls_path): + with open(error_dlls_path, "r") as error_dlls_file: + error_dlls_json = json.load(error_dlls_file) + if len(error_dlls_json) > 0 and error_dlls_json[0].strip() != "": + print(error_dlls_json) + # exit(1) diff --git a/source/meta/run_diags.py b/source/meta/run_diags.py new file mode 100644 index 00000000..0424674a --- /dev/null +++ b/source/meta/run_diags.py @@ -0,0 +1,10 @@ +from source.classes import diags as diags + +global VERBOSE +VERBOSE = True + +if __name__ == "__main__": + if VERBOSE: + print("DIAGNOSTICS") + print('.' * 70) + print("\n".join(diags.output())) diff --git a/source/overworld/EntranceData.py b/source/overworld/EntranceData.py new file mode 100644 index 00000000..a9fe0b2a --- /dev/null +++ b/source/overworld/EntranceData.py @@ -0,0 +1,401 @@ +# format: +# Key=Name +# addr = (door_index, exitdata) # multiexit +# | ([addr], None) # holes +# exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) + +door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000), 0x00), + 'Desert Palace Entrance (South)': (0x08, (0x0084, 0x30, 0x0314, 0x0c56, 0x00a6, 0x0ca8, 0x0128, 0x0cc3, 0x0133, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), + 'Desert Palace Entrance (West)': (0x0A, (0x0083, 0x30, 0x0280, 0x0c46, 0x0003, 0x0c98, 0x0088, 0x0cb3, 0x0090, 0x0a, 0xfd, 0x0000, 0x0000), 0x00), + 'Desert Palace Entrance (North)': (0x0B, (0x0063, 0x30, 0x0016, 0x0c00, 0x00a2, 0x0c28, 0x0128, 0x0c6d, 0x012f, 0x00, 0x0e, 0x0000, 0x0000), 0x00), + 'Desert Palace Entrance (East)': (0x09, (0x0085, 0x30, 0x02a8, 0x0c4a, 0x0142, 0x0c98, 0x01c8, 0x0cb7, 0x01cf, 0x06, 0xfe, 0x0000, 0x0000), 0x00), + 'Eastern Palace': (0x07, (0x00c9, 0x1e, 0x005a, 0x0600, 0x0ed6, 0x0618, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000), 0x00), + 'Tower of Hera': (0x32, (0x0077, 0x03, 0x0050, 0x0014, 0x087c, 0x0068, 0x08f0, 0x0083, 0x08fb, 0x0a, 0xf4, 0x0000, 0x0000), 0x00), + 'Hyrule Castle Entrance (South)': (0x03, (0x0061, 0x1b, 0x0530, 0x0692, 0x0784, 0x06cc, 0x07f8, 0x06ff, 0x0803, 0x0e, 0xfa, 0x0000, 0x87be), 0x00), + 'Hyrule Castle Entrance (West)': (0x02, (0x0060, 0x1b, 0x0016, 0x0600, 0x06ae, 0x0604, 0x0728, 0x066d, 0x0733, 0x00, 0x02, 0x0000, 0x8124), 0x00), + 'Hyrule Castle Entrance (East)': (0x04, (0x0062, 0x1b, 0x004a, 0x0600, 0x0856, 0x0604, 0x08c8, 0x066d, 0x08d3, 0x00, 0xfa, 0x0000, 0x8158), 0x00), + 'Inverted Pyramid Entrance': (0x35, (0x0010, 0x1b, 0x000e, 0x0600, 0x0676, 0x0604, 0x06e8, 0x066d, 0x06f3, 0x00, 0x0a, 0x0000, 0x811c), 0x00), + 'Agahnims Tower': (0x23, (0x00e0, 0x1b, 0x0032, 0x0600, 0x0784, 0x0634, 0x07f8, 0x066d, 0x0803, 0x00, 0x0a, 0x0000, 0x82be), 0x40), + 'Thieves Town': (0x33, (0x00db, 0x58, 0x0b2e, 0x075a, 0x0176, 0x07a8, 0x01f8, 0x07c7, 0x0203, 0x06, 0xfa, 0x0000, 0x0000), 0x20), + 'Skull Woods First Section Door': (0x29, (0x0058, 0x40, 0x0f4c, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0xfe, 0x0000, 0x0000), 0x00), + 'Skull Woods Second Section Door (East)': (0x28, (0x0057, 0x40, 0x0eb8, 0x01e6, 0x01c2, 0x0238, 0x0248, 0x0253, 0x024f, 0x0a, 0xfe, 0x0000, 0x0000), 0x00), + 'Skull Woods Second Section Door (West)': (0x27, (0x0056, 0x40, 0x0c8e, 0x01a6, 0x0062, 0x01f8, 0x00e8, 0x0213, 0x00ef, 0x0a, 0x0e, 0x0000, 0x0000), 0x00), + 'Skull Woods Final Section': (0x2A, (0x0059, 0x40, 0x0282, 0x0066, 0x0016, 0x00b8, 0x0098, 0x00d3, 0x00a3, 0x0a, 0xfa, 0x0000, 0x0000), 0x20), + 'Ice Palace': (0x2C, (0x000e, 0x75, 0x0bc6, 0x0d6a, 0x0c3e, 0x0db8, 0x0cb8, 0x0dd7, 0x0cc3, 0x06, 0xf2, 0x0000, 0x0000), 0x00), + 'Misery Mire': (0x26, (0x0098, 0x70, 0x0414, 0x0c79, 0x00a6, 0x0cc7, 0x0128, 0x0ce6, 0x0133, 0x07, 0xfa, 0x0000, 0x0000), 0x20), + 'Palace of Darkness': (0x25, (0x004a, 0x5e, 0x005a, 0x0600, 0x0ed6, 0x0628, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000), 0x20), + 'Swamp Palace': (0x24, (0x0028, 0x7b, 0x049e, 0x0e8c, 0x06f2, 0x0ed8, 0x0778, 0x0ef9, 0x077f, 0x04, 0xfe, 0x0000, 0x0000), 0x00), + 'Turtle Rock': (0x34, (0x00d6, 0x47, 0x0712, 0x00da, 0x0e96, 0x0128, 0x0f08, 0x0147, 0x0f13, 0x06, 0xfa, 0x0000, 0x0000), 0x20), + 'Dark Death Mountain Ledge (West)': (0x14, (0x0023, 0x45, 0x07ca, 0x0103, 0x0c46, 0x0157, 0x0cb8, 0x0172, 0x0cc3, 0x0b, 0x0a, 0x0000, 0x0000), 0x00), + 'Dark Death Mountain Ledge (East)': (0x18, (0x0024, 0x45, 0x07e0, 0x0103, 0x0d00, 0x0157, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), + 'Turtle Rock Isolated Ledge Entrance': (0x17, (0x00d5, 0x45, 0x0ad4, 0x0164, 0x0ca6, 0x01b8, 0x0d18, 0x01d3, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), + 'Hyrule Castle Secret Entrance Stairs': (0x31, (0x0055, 0x1b, 0x044a, 0x067a, 0x0854, 0x06c8, 0x08c8, 0x06e7, 0x08d3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Kakariko Well Cave': (0x38, (0x002f, 0x18, 0x0386, 0x0665, 0x0032, 0x06b7, 0x00b8, 0x06d2, 0x00bf, 0x0b, 0xfe, 0x0000, 0x0000), 0x00), + 'Bat Cave Cave': (0x10, (0x00e3, 0x22, 0x0412, 0x087a, 0x048e, 0x08c8, 0x0508, 0x08e7, 0x0513, 0x06, 0x02, 0x0000, 0x0000), 0x00), + 'Elder House (East)': (0x0D, (0x00f3, 0x18, 0x02c4, 0x064a, 0x0222, 0x0698, 0x02a8, 0x06b7, 0x02af, 0x06, 0xfe, 0x05d4, 0x0000), 0x00), + 'Elder House (West)': (0x0C, (0x00f2, 0x18, 0x02bc, 0x064c, 0x01e2, 0x0698, 0x0268, 0x06b9, 0x026f, 0x04, 0xfe, 0x05cc, 0x0000), 0x00), + 'North Fairy Cave': (0x37, (0x0008, 0x15, 0x0088, 0x0400, 0x0a36, 0x0448, 0x0aa8, 0x046f, 0x0ab3, 0x00, 0x0a, 0x0000, 0x0000), 0x00), + 'Lost Woods Hideout Stump': (0x2B, (0x00e1, 0x00, 0x0f4e, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0x0e, 0x0000, 0x0000), 0x00), + 'Lumberjack Tree Cave': (0x11, (0x00e2, 0x02, 0x0118, 0x0015, 0x04c6, 0x0067, 0x0548, 0x0082, 0x0553, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), + 'Two Brothers House (East)': (0x0F, (0x00f5, 0x29, 0x0880, 0x0b07, 0x0200, 0x0b58, 0x0238, 0x0b74, 0x028d, 0x09, 0x00, 0x0b86, 0x0000), 0x00), + 'Two Brothers House (West)': (0x0E, (0x00f4, 0x28, 0x08a0, 0x0b06, 0x0100, 0x0b58, 0x01b8, 0x0b73, 0x018d, 0x0a, 0x00, 0x0bb6, 0x0000), 0x00), + 'Sanctuary': (0x01, (0x0012, 0x13, 0x001c, 0x0400, 0x06de, 0x0414, 0x0758, 0x046d, 0x0763, 0x00, 0x02, 0x0000, 0x01aa), 0x00), + 'Old Man Cave (West)': (0x05, (0x00f0, 0x0a, 0x03a0, 0x0264, 0x0500, 0x02b8, 0x05a8, 0x02d3, 0x058d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), + 'Old Man Cave (East)': (0x06, (0x00f1, 0x03, 0x1402, 0x0294, 0x0604, 0x02e8, 0x0678, 0x0303, 0x0683, 0x0a, 0xfc, 0x0000, 0x0000), 0x00), + 'Old Man House (Bottom)': (0x2F, (0x00e4, 0x03, 0x181a, 0x031e, 0x06b4, 0x03a7, 0x0728, 0x038d, 0x0733, 0x00, 0x0c, 0x0000, 0x0000), 0x00), + 'Old Man House (Top)': (0x30, (0x00e5, 0x03, 0x10c6, 0x0224, 0x0814, 0x0278, 0x0888, 0x0293, 0x0893, 0x0a, 0x0c, 0x0000, 0x0000), 0x00), + 'Death Mountain Return Cave (East)': (0x2E, (0x00e7, 0x03, 0x0d82, 0x01c4, 0x0600, 0x0218, 0x0648, 0x0233, 0x067f, 0x0a, 0x00, 0x0000, 0x0000), 0x00), + 'Death Mountain Return Cave (West)': (0x2D, (0x00e6, 0x0a, 0x00a0, 0x0205, 0x0500, 0x0257, 0x05b8, 0x0272, 0x058d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), + 'Spectacle Rock Cave Peak': (0x22, (0x00ea, 0x03, 0x092c, 0x0133, 0x0754, 0x0187, 0x07c8, 0x01a2, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), + 'Spectacle Rock Cave': (0x21, (0x00fa, 0x03, 0x0eac, 0x01e3, 0x0754, 0x0237, 0x07c8, 0x0252, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), + 'Spectacle Rock Cave (Bottom)': (0x20, (0x00f9, 0x03, 0x0d9c, 0x01c3, 0x06d4, 0x0217, 0x0748, 0x0232, 0x0753, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), + 'Paradox Cave (Bottom)': (0x1D, (0x00ff, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0237, 0x0da8, 0x0252, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), + 'Paradox Cave (Middle)': (0x1E, (0x00ef, 0x05, 0x17e0, 0x0304, 0x0d00, 0x0358, 0x0dc8, 0x0373, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), + 'Paradox Cave (Top)': (0x1F, (0x00df, 0x05, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), + 'Fairy Ascension Cave (Bottom)': (0x19, (0x00fd, 0x05, 0x0dd4, 0x01c4, 0x0ca6, 0x0218, 0x0d18, 0x0233, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), + 'Fairy Ascension Cave (Top)': (0x1A, (0x00ed, 0x05, 0x0ad4, 0x0163, 0x0ca6, 0x01b7, 0x0d18, 0x01d2, 0x0d23, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), + 'Spiral Cave': (0x1C, (0x00ee, 0x05, 0x07c8, 0x0108, 0x0c46, 0x0158, 0x0cb8, 0x0177, 0x0cc3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Spiral Cave (Bottom)': (0x1B, (0x00fe, 0x05, 0x0cca, 0x01a3, 0x0c56, 0x01f7, 0x0cc8, 0x0212, 0x0cd3, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), + 'Bumper Cave (Bottom)': (0x15, (0x00fb, 0x4a, 0x03a0, 0x0263, 0x0500, 0x02b7, 0x05a8, 0x02d2, 0x058d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), + 'Bumper Cave (Top)': (0x16, (0x00eb, 0x4a, 0x00a0, 0x020a, 0x0500, 0x0258, 0x05b8, 0x0277, 0x058d, 0x06, 0x00, 0x0000, 0x0000), 0x00), + 'Superbunny Cave (Top)': (0x13, (0x00e8, 0x45, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), + 'Superbunny Cave (Bottom)': (0x12, (0x00f8, 0x45, 0x0ee0, 0x01e4, 0x0d00, 0x0238, 0x0d78, 0x0253, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), + 'Hookshot Cave': (0x39, (0x003c, 0x45, 0x04da, 0x00a3, 0x0cd6, 0x0107, 0x0d48, 0x0112, 0x0d53, 0x0b, 0xfa, 0x0000, 0x0000), 0x20), + 'Hookshot Cave Back Entrance': (0x3A, (0x002c, 0x45, 0x004c, 0x0000, 0x0c56, 0x0038, 0x0cc8, 0x006f, 0x0cd3, 0x00, 0x0a, 0x0000, 0x0000), 0x00), + 'Ganons Tower': (0x36, (0x000c, 0x43, 0x0052, 0x0000, 0x0884, 0x0028, 0x08f8, 0x006f, 0x0903, 0x00, 0xfc, 0x0000, 0x0000), 0x20), + 'Pyramid Entrance': (0x35, (0x0010, 0x5b, 0x0b0e, 0x075a, 0x0674, 0x07a8, 0x06e8, 0x07c7, 0x06f3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Skull Woods First Section Hole (West)': ([0xDB84D, 0xDB84E], None), + 'Skull Woods First Section Hole (East)': ([0xDB84F, 0xDB850], None), + 'Skull Woods First Section Hole (North)': ([0xDB84C], None), + 'Skull Woods Second Section Hole': ([0xDB851, 0xDB852], None), + 'Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856], None), + 'Inverted Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856, 0x180340], None), + 'Waterfall of Wishing': (0x5B, (0x0114, 0x0f, 0x0080, 0x0200, 0x0e00, 0x0207, 0x0e60, 0x026f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Dam': (0x4D, (0x010b, 0x3b, 0x04a0, 0x0e8a, 0x06fa, 0x0ed8, 0x0778, 0x0ef7, 0x077f, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Blinds Hideout': (0x60, (0x0119, 0x18, 0x02b2, 0x064a, 0x0186, 0x0697, 0x0208, 0x06b7, 0x0213, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Hyrule Castle Secret Entrance Drop': ([0xDB858], None), + 'Bonk Fairy (Light)': (0x76, (0x0126, 0x2b, 0x00a0, 0x0a0a, 0x0700, 0x0a67, 0x0788, 0x0a77, 0x0785, 0x06, 0xfa, 0x0000, 0x0000), 0x20), + 'Lake Hylia Fairy': (0x5D, (0x0115, 0x2e, 0x0016, 0x0a00, 0x0cb6, 0x0a37, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), + 'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000), 0x20), + 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), + 'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000), 0x00), + 'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000), 0x00), + 'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Lake Hylia Shop': (0x57, (0x0112, 0x35, 0x0022, 0x0c00, 0x0b1a, 0x0c26, 0x0b98, 0x0c6d, 0x0b9f, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Capacity Upgrade': (0x5C, (0x0115, 0x35, 0x0a46, 0x0d36, 0x0c2a, 0x0d88, 0x0ca8, 0x0da3, 0x0caf, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Kakariko Well Drop': ([0xDB85C, 0xDB85D], None), + 'Blacksmiths Hut': (0x63, (0x0121, 0x22, 0x010c, 0x081a, 0x0466, 0x0868, 0x04d8, 0x0887, 0x04e3, 0x06, 0xfa, 0x041A, 0x0000), 0x00), + 'Bat Cave Drop': ([0xDB859, 0xDB85A], None), + 'Sick Kids House': (0x3F, (0x0102, 0x18, 0x10be, 0x0826, 0x01f6, 0x0877, 0x0278, 0x0893, 0x0283, 0x08, 0xf8, 0x14CE, 0x0000), 0x00), + 'North Fairy Cave Drop': ([0xDB857], None), + 'Lost Woods Gamble': (0x3B, (0x0100, 0x00, 0x004e, 0x0000, 0x0272, 0x0008, 0x02f0, 0x006f, 0x02f7, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Fortune Teller (Light)': (0x64, (0x0122, 0x11, 0x060e, 0x04b4, 0x027d, 0x0508, 0x02f8, 0x0523, 0x0302, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Snitch Lady (East)': (0x3D, (0x0101, 0x18, 0x0ad8, 0x074a, 0x02c6, 0x0798, 0x0348, 0x07b7, 0x0353, 0x06, 0xfa, 0x0DE8, 0x0000), 0x00), + 'Snitch Lady (West)': (0x3E, (0x0101, 0x18, 0x0788, 0x0706, 0x0046, 0x0758, 0x00c8, 0x0773, 0x00d3, 0x08, 0xf8, 0x0B98, 0x0000), 0x00), + 'Bush Covered House': (0x43, (0x0103, 0x18, 0x1156, 0x081a, 0x02b6, 0x0868, 0x0338, 0x0887, 0x0343, 0x06, 0xfa, 0x1466, 0x0000), 0x00), + 'Tavern (Front)': (0x41, (0x0103, 0x18, 0x1842, 0x0916, 0x0206, 0x0967, 0x0288, 0x0983, 0x0293, 0x08, 0xf8, 0x1C50, 0x0000), 0x00), + 'Light World Bomb Hut': (0x49, (0x0107, 0x18, 0x1800, 0x0916, 0x0000, 0x0967, 0x0068, 0x0983, 0x008d, 0x08, 0xf8, 0x9C0C, 0x0000), 0x02), + 'Kakariko Shop': (0x45, (0x011f, 0x18, 0x16a8, 0x08e7, 0x0136, 0x0937, 0x01b8, 0x0954, 0x01c3, 0x07, 0xf9, 0x1AB6, 0x0000), 0x00), + 'Lost Woods Hideout Drop': ([0xDB853], None), + 'Lumberjack Tree Tree': ([0xDB85B], None), + 'Cave 45': (0x50, (0x011b, 0x32, 0x0680, 0x0cc9, 0x0400, 0x0d16, 0x0438, 0x0d36, 0x0485, 0x07, 0xf9, 0x0000, 0x0000), 0x00), + 'Graveyard Cave': (0x51, (0x011b, 0x14, 0x0016, 0x0400, 0x08a2, 0x0446, 0x0918, 0x046d, 0x091f, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Checkerboard Cave': (0x7D, (0x0126, 0x30, 0x00c8, 0x0c0a, 0x024a, 0x0c67, 0x02c8, 0x0c77, 0x02cf, 0x06, 0xfa, 0x0000, 0x0000), 0x20), + 'Mini Moldorm Cave': (0x7C, (0x0123, 0x35, 0x1480, 0x0e96, 0x0a00, 0x0ee8, 0x0a68, 0x0f03, 0x0a85, 0x08, 0xf8, 0x0000, 0x0000), 0x02), + 'Long Fairy Cave': (0x54, (0x011e, 0x2f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Good Bee Cave': (0x6A, (0x0120, 0x37, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000), 0x00), + '20 Rupee Cave': (0x7A, (0x0125, 0x37, 0x0200, 0x0c23, 0x0e00, 0x0c86, 0x0e68, 0x0c92, 0x0e7d, 0x0d, 0xf3, 0x0000, 0x0000), 0x20), + '50 Rupee Cave': (0x78, (0x0124, 0x3a, 0x0790, 0x0eea, 0x047a, 0x0f47, 0x04f8, 0x0f57, 0x04ff, 0x06, 0xfa, 0x0000, 0x0000), 0x20), + 'Ice Rod Cave': (0x7F, (0x0120, 0x37, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x02), + 'Bonk Rock Cave': (0x79, (0x0124, 0x13, 0x0280, 0x044a, 0x0600, 0x04a7, 0x0638, 0x04b7, 0x067d, 0x06, 0xfa, 0x0000, 0x0000), 0x20), + 'Library': (0x48, (0x0107, 0x29, 0x0100, 0x0a14, 0x0200, 0x0a67, 0x0278, 0x0a83, 0x0285, 0x0a, 0xf6, 0x040E, 0x0000), 0x00), + 'Potion Shop': (0x4B, (0x0109, 0x16, 0x070a, 0x04e6, 0x0c56, 0x0538, 0x0cc8, 0x0553, 0x0cd3, 0x08, 0xf8, 0x0A98, 0x0000), 0x00), + 'Sanctuary Grave': ([0xDB85E], None), + 'Hookshot Fairy': (0x4F, (0x010c, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0d78, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), + 'Pyramid Fairy': (0x62, (0x0116, 0x5b, 0x0b1e, 0x0754, 0x06fa, 0x07a7, 0x0778, 0x07c3, 0x077f, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), + 'East Dark World Hint': (0x68, (0x010e, 0x6f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Palace of Darkness Hint': (0x67, (0x011a, 0x5e, 0x0c24, 0x0794, 0x0d12, 0x07e8, 0x0d90, 0x0803, 0x0d97, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Dark Lake Hylia Fairy': (0x6C, (0x0115, 0x6e, 0x0016, 0x0a00, 0x0cb6, 0x0a36, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Dark Lake Hylia Ledge Fairy': (0x80, (0x0115, 0x77, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x02), + 'Dark Lake Hylia Ledge Spike Cave': (0x7B, (0x0125, 0x77, 0x0200, 0x0c27, 0x0e00, 0x0c86, 0x0e68, 0x0c96, 0x0e7d, 0x09, 0xf7, 0x0000, 0x0000), 0x20), + 'Dark Lake Hylia Ledge Hint': (0x69, (0x010e, 0x77, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Hype Cave': (0x3C, (0x011e, 0x74, 0x00a0, 0x0c0a, 0x0900, 0x0c58, 0x0988, 0x0c77, 0x097d, 0x06, 0xfa, 0x0000, 0x0000), 0x02), + 'Bonk Fairy (Dark)': (0x77, (0x0126, 0x6b, 0x00a0, 0x0a05, 0x0700, 0x0a66, 0x0788, 0x0a72, 0x0785, 0x0b, 0xf5, 0x0000, 0x0000), 0x20), + 'Brewery': (0x47, (0x0106, 0x58, 0x16a8, 0x08e4, 0x013e, 0x0938, 0x01b8, 0x0953, 0x01c3, 0x0a, 0xf6, 0x1AB6, 0x0000), 0x02), + 'C-Shaped House': (0x53, (0x011c, 0x58, 0x09d8, 0x0744, 0x02ce, 0x0797, 0x0348, 0x07b3, 0x0353, 0x0a, 0xf6, 0x0DE8, 0x0000), 0x00), + 'Chest Game': (0x46, (0x0106, 0x58, 0x078a, 0x0705, 0x004e, 0x0758, 0x00c8, 0x0774, 0x00d3, 0x09, 0xf7, 0x0B98, 0x0000), 0x00), + 'Hammer Peg Cave': (0x7E, (0x0127, 0x62, 0x0894, 0x091e, 0x0492, 0x09a6, 0x0508, 0x098b, 0x050f, 0x00, 0x00, 0x0000, 0x0000), 0x20), + 'Red Shield Shop': (0x74, (0x0110, 0x5a, 0x079a, 0x06e8, 0x04d6, 0x0738, 0x0548, 0x0755, 0x0553, 0x08, 0xf8, 0x0AA8, 0x0000), 0x00), + 'Dark Sanctuary Hint': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000), 0x00), + 'Fortune Teller (Dark)': (0x65, (0x0122, 0x51, 0x0610, 0x04b4, 0x027e, 0x0507, 0x02f8, 0x0523, 0x0303, 0x0a, 0xf6, 0x091E, 0x0000), 0x00), + 'Dark World Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Dark Lumberjack Shop': (0x56, (0x010f, 0x42, 0x041c, 0x0074, 0x04e2, 0x00c7, 0x0558, 0x00e3, 0x055f, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Dark Potion Shop': (0x6E, (0x010f, 0x56, 0x080e, 0x04f4, 0x0c66, 0x0548, 0x0cd8, 0x0563, 0x0ce3, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Archery Game': (0x58, (0x0111, 0x69, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00), + 'Mire Shed': (0x5E, (0x010d, 0x70, 0x0384, 0x0c69, 0x001e, 0x0cb6, 0x0098, 0x0cd6, 0x00a3, 0x07, 0xf9, 0x0000, 0x0000), 0x00), + 'Mire Hint': (0x61, (0x0114, 0x70, 0x0654, 0x0cc5, 0x02aa, 0x0d16, 0x0328, 0x0d32, 0x032f, 0x09, 0xf7, 0x0000, 0x0000), 0x00), + 'Mire Fairy': (0x55, (0x0115, 0x70, 0x03a8, 0x0c6a, 0x013a, 0x0cb7, 0x01b8, 0x0cd7, 0x01bf, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Spike Cave': (0x40, (0x0117, 0x43, 0x0ed4, 0x01e4, 0x08aa, 0x0236, 0x0928, 0x0253, 0x092f, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Dark Death Mountain Shop': (0x6D, (0x0112, 0x45, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0da8, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), + 'Dark Death Mountain Fairy': (0x6F, (0x0115, 0x43, 0x1400, 0x0294, 0x0600, 0x02e8, 0x0678, 0x0303, 0x0685, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), + 'Mimic Cave': (0x4E, (0x010c, 0x05, 0x07e0, 0x0103, 0x0d00, 0x0156, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), + 'Big Bomb Shop': (0x52, (0x011c, 0x6c, 0x0506, 0x0a9a, 0x0832, 0x0ae7, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfa, 0x0816, 0x0000), 0x00), + 'Dark Lake Hylia Shop': (0x73, (0x010f, 0x75, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Lumberjack House': (0x75, (0x011f, 0x02, 0x049c, 0x0088, 0x04e6, 0x00d8, 0x0558, 0x00f7, 0x0563, 0x08, 0xf8, 0x07AA, 0x0000), 0x00), + 'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), + 'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)} + +ow_prize_table = {'Links House': (0x8b1, 0xb2d), + 'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0), + 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), + 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), + 'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0), + 'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0), + 'Agahnims Tower': (0x820, 0x5D0), + 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280), + 'Skull Woods Second Section Door (East)': (0x200, 0x240), + 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), + 'Skull Woods Final Section': (0x082, 0x0b0), + 'Skull Woods First Section Hole (West)': (0x200, 0x2b0), + 'Skull Woods First Section Hole (East)': (0x340, 0x2e0), + 'Skull Woods First Section Hole (North)': (0x320, 0x1e0), + 'Skull Woods Second Section Hole': (0x0f0, 0x0b0), + 'Ice Palace': (0xca0, 0xda0), + 'Misery Mire': (0x100, 0xca0), + 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), + 'Turtle Rock': (0xf11, 0x103), + 'Dark Death Mountain Ledge (West)': (0xb80, 0x180), + 'Dark Death Mountain Ledge (East)': (0xc80, 0x180), + 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), + 'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680), + 'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700), + 'Kakariko Well Drop': (0x030, 0x680), + 'Kakariko Well Cave': (0x060, 0x680), + 'Bat Cave Drop': (0x520, 0x8f0), + 'Bat Cave Cave': (0x560, 0x940), + 'Elder House (East)': (0x2b0, 0x6a0), + 'Elder House (West)': (0x230, 0x6a0), + 'North Fairy Cave Drop': (0xa40, 0x500), + 'North Fairy Cave': (0xa80, 0x440), + 'Lost Woods Hideout Drop': (0x290, 0x200), + 'Lost Woods Hideout Stump': (0x240, 0x280), + 'Lumberjack Tree Tree': (0x4e0, 0x140), + 'Lumberjack Tree Cave': (0x560, 0x004), + 'Two Brothers House (East)': (0x200, 0x0b60), + 'Two Brothers House (West)': (0x180, 0x0b60), + 'Sanctuary Grave': (0x820, 0x4c0), + 'Sanctuary': (0x720, 0x4a0), + 'Old Man Cave (West)': (0x580, 0x2c0), + 'Old Man Cave (East)': (0x620, 0x2c0), + 'Old Man House (Bottom)': (0x720, 0x320), + 'Old Man House (Top)': (0x820, 0x220), + 'Death Mountain Return Cave (East)': (0x600, 0x220), + 'Death Mountain Return Cave (West)': (0x500, 0x1c0), + 'Spectacle Rock Cave Peak': (0x720, 0x0a0), + 'Spectacle Rock Cave': (0x790, 0x1a0), + 'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0), + 'Paradox Cave (Bottom)': (0xd80, 0x180), + 'Paradox Cave (Middle)': (0xd80, 0x380), + 'Paradox Cave (Top)': (0xd80, 0x020), + 'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0), + 'Fairy Ascension Cave (Top)': (0xc00, 0x240), + 'Spiral Cave': (0xb80, 0x180), + 'Spiral Cave (Bottom)': (0xb80, 0x2c0), + 'Bumper Cave (Bottom)': (0x580, 0x2c0), + 'Bumper Cave (Top)': (0x500, 0x1c0), + 'Superbunny Cave (Top)': (0xd80, 0x020), + 'Superbunny Cave (Bottom)': (0xd00, 0x180), + 'Hookshot Cave': (0xc80, 0x0c0), + 'Hookshot Cave Back Entrance': (0xcf0, 0x004), + 'Ganons Tower': (0x8D0, 0x080), + 'Pyramid Hole': (0x820, 0x680), + 'Inverted Pyramid Hole': (0x820, 0x680), + 'Pyramid Entrance': (0x640, 0x7c0), + 'Inverted Pyramid Entrance': (0x6C0, 0x5D0), + 'Waterfall of Wishing': (0xe80, 0x280), + 'Dam': (0x759, 0xED0), + 'Blinds Hideout': (0x190, 0x6c0), + 'Bonk Fairy (Light)': (0x740, 0xa80), + 'Lake Hylia Fairy': (0xd40, 0x9f0), + 'Light Hype Fairy': (0x940, 0xc80), + 'Desert Fairy': (0x420, 0xe00), + 'Kings Grave': (0x920, 0x520), + 'Tavern North': (0x270, 0x900), + 'Chicken House': (0x120, 0x880), + 'Aginahs Cave': (0x2e0, 0xd00), + 'Sahasrahlas Hut': (0xcf0, 0x6c0), + 'Lake Hylia Shop': (0xbc0, 0xc00), + 'Capacity Upgrade': (0xca0, 0xda0), + 'Blacksmiths Hut': (0x4a0, 0x880), + 'Sick Kids House': (0x220, 0x880), + 'Lost Woods Gamble': (0x240, 0x080), + 'Fortune Teller (Light)': (0x2c0, 0x4c0), + 'Snitch Lady (East)': (0x310, 0x7a0), + 'Snitch Lady (West)': (0x080, 0x7a0), + 'Bush Covered House': (0x2e0, 0x880), + 'Tavern (Front)': (0x270, 0x980), + 'Light World Bomb Hut': (0x070, 0x980), + 'Kakariko Shop': (0x170, 0x980), + 'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), + 'Checkerboard Cave': (0x260, 0xc00), + 'Mini Moldorm Cave': (0xa40, 0xe80), + 'Long Fairy Cave': (0xf60, 0xb00), + 'Good Bee Cave': (0xec0, 0xc00), + '20 Rupee Cave': (0xe80, 0xca0), + '50 Rupee Cave': (0x4d0, 0xed0), + 'Ice Rod Cave': (0xe00, 0xc00), + 'Bonk Rock Cave': (0x5f0, 0x460), + 'Library': (0x270, 0xaa0), + 'Potion Shop': (0xc80, 0x4c0), + 'Hookshot Fairy': (0xd00, 0x180), + 'Pyramid Fairy': (0x740, 0x740), + 'East Dark World Hint': (0xf60, 0xb00), + 'Palace of Darkness Hint': (0xd60, 0x7c0), + 'Dark Lake Hylia Fairy': (0xd40, 0x9f0), + 'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00), + 'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0), + 'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00), + 'Hype Cave': (0x940, 0xc80), + 'Bonk Fairy (Dark)': (0x740, 0xa80), + 'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x080, 0x7a0), + 'Hammer Peg Cave': (0x4c0, 0x940), + 'Red Shield Shop': (0x500, 0x680), + 'Dark Sanctuary Hint': (0x720, 0x4a0), + 'Fortune Teller (Dark)': (0x2c0, 0x4c0), + 'Dark World Shop': (0x2e0, 0x880), + 'Dark Lumberjack Shop': (0x4e0, 0x0d0), + 'Dark Potion Shop': (0xc80, 0x4c0), + 'Archery Game': (0x2f0, 0xaf0), + 'Mire Shed': (0x060, 0xc90), + 'Mire Hint': (0x2e0, 0xd00), + 'Mire Fairy': (0x1c0, 0xc90), + 'Spike Cave': (0x860, 0x180), + 'Dark Death Mountain Shop': (0xd80, 0x180), + 'Dark Death Mountain Fairy': (0x620, 0x2c0), + 'Mimic Cave': (0xc80, 0x180), + 'Big Bomb Shop': (0x8b1, 0xb2d), + 'Dark Lake Hylia Shop': (0xa40, 0xc40), + 'Lumberjack House': (0x580, 0x100), + 'Lake Hylia Fortune Teller': (0xa40, 0xc40), + 'Kakariko Gamble Game': (0x2f0, 0xaf0)} + +default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'), + ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'), + ('Spectacle Rock Cave Peak', 'Spectacle Rock Cave Exit (Peak)'), + ('Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave Exit'), + ('Spectacle Rock Cave', 'Spectacle Rock Cave Exit (Top)'), + ('Old Man Cave (East)', 'Old Man Cave Exit (East)'), + ('Old Man Cave (West)', 'Old Man Cave Exit (West)'), + ('Old Man House (Bottom)', 'Old Man House Exit (Bottom)'), + ('Old Man House (Top)', 'Old Man House Exit (Top)'), + ('Spiral Cave', 'Spiral Cave Exit (Top)'), + ('Spiral Cave (Bottom)', 'Spiral Cave Exit'), + ('Fairy Ascension Cave (Top)', 'Fairy Ascension Cave Exit (Top)'), + ('Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave Exit (Bottom)'), + ('Paradox Cave (Bottom)', 'Paradox Cave Exit (Bottom)'), + ('Paradox Cave (Middle)', 'Paradox Cave Exit (Middle)'), + ('Paradox Cave (Top)', 'Paradox Cave Exit (Top)'), + ('Elder House (West)', 'Elder House Exit (West)'), + ('Elder House (East)', 'Elder House Exit (East)'), + ('Two Brothers House (West)', 'Two Brothers House Exit (West)'), + ('Two Brothers House (East)', 'Two Brothers House Exit (East)'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave Back Exit'), + ('Hookshot Cave', 'Hookshot Cave Front Exit'), + ('Superbunny Cave (Top)', 'Superbunny Cave Exit (Top)'), + ('Superbunny Cave (Bottom)', 'Superbunny Cave Exit (Bottom)'), + ('Bumper Cave (Bottom)', 'Bumper Cave Exit (Bottom)'), + ('Bumper Cave (Top)', 'Bumper Cave Exit (Top)') + ] + +default_item_connections = [('Mimic Cave', 'Mimic Cave'), + ('Waterfall of Wishing', 'Waterfall of Wishing'), + ('Bonk Rock Cave', 'Bonk Rock Cave'), + ('Graveyard Cave', 'Graveyard Cave'), + ('Kings Grave', 'Kings Grave'), + ('Potion Shop', 'Potion Shop'), + ('Blinds Hideout', 'Blinds Hideout'), + ('Chicken House', 'Chicken House'), + ('Sick Kids House', 'Sick Kids House'), + ('Sahasrahlas Hut', 'Sahasrahlas Hut'), + ('Blacksmiths Hut', 'Blacksmiths Hut'), + ('Library', 'Library'), + ('Links House', 'Links House Exit'), + ('Checkerboard Cave', 'Checkerboard Cave'), + ('Aginahs Cave', 'Aginahs Cave'), + ('Cave 45', 'Cave 45'), + ('Mini Moldorm Cave', 'Mini Moldorm Cave'), + ('Ice Rod Cave', 'Ice Rod Cave'), + ('Dam', 'Dam'), + ('Spike Cave', 'Spike Cave'), + ('Chest Game', 'Chest Game'), + ('C-Shaped House', 'C-Shaped House'), + ('Brewery', 'Brewery'), + ('Pyramid Fairy', 'Pyramid Fairy'), + ('Hammer Peg Cave', 'Hammer Peg Cave'), + ('Big Bomb Shop', 'Big Bomb Shop'), + ('Mire Shed', 'Mire Shed'), + ('Hype Cave', 'Hype Cave') + ] + +default_shop_connections = [('Kakariko Shop', 'Kakariko Shop'), + ('Lake Hylia Shop', 'Lake Hylia Shop'), + ('Capacity Upgrade', 'Capacity Upgrade'), + ('Dark Lumberjack Shop', 'Dark Lumberjack Shop'), + ('Dark Death Mountain Shop', 'Dark Death Mountain Shop'), + ('Dark Potion Shop', 'Dark Potion Shop'), + ('Dark World Shop', 'Village of Outcasts Shop'), + ('Red Shield Shop', 'Red Shield Shop'), + ('Dark Lake Hylia Shop', 'Dark Lake Hylia Shop') + ] + +default_drop_connections = [('Lost Woods Hideout Drop', 'Lost Woods Hideout (top)'), + ('Lumberjack Tree Tree', 'Lumberjack Tree (top)'), + ('Sanctuary Grave', 'Sewer Drop'), + ('North Fairy Cave Drop', 'North Fairy Cave'), + ('Kakariko Well Drop', 'Kakariko Well (top)'), + ('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance'), + ('Bat Cave Drop', 'Bat Cave (right)'), + #('Pyramid Hole', 'Pyramid') # this is dynamically added because of Inverted/OW Mixed + ] + +default_dropexit_connections = [('Lost Woods Hideout Stump', 'Lost Woods Hideout Exit'), + ('Lumberjack Tree Cave', 'Lumberjack Tree Exit'), + ('Sanctuary', 'Sanctuary Exit'), + ('North Fairy Cave', 'North Fairy Cave Exit'), + ('Kakariko Well Cave', 'Kakariko Well Exit'), + ('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit'), + ('Bat Cave Cave', 'Bat Cave Exit'), + #('Pyramid Entrance', 'Pyramid Exit') # this is dynamically added because of Inverted/OW Mixed + ] + +# non shuffled dungeons +default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Palace Exit (South)'), + ('Desert Palace Entrance (West)', 'Desert Palace Exit (West)'), + ('Desert Palace Entrance (North)', 'Desert Palace Exit (North)'), + ('Desert Palace Entrance (East)', 'Desert Palace Exit (East)'), + + ('Eastern Palace', 'Eastern Palace Exit'), + ('Tower of Hera', 'Tower of Hera Exit'), + + ('Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)'), + ('Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)'), + ('Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)'), + + ('Thieves Town', 'Thieves Town Exit'), + ('Skull Woods First Section Door', 'Skull Woods First Section Exit'), + ('Skull Woods Second Section Door (East)', 'Skull Woods Second Section Exit (East)'), + ('Skull Woods Second Section Door (West)', 'Skull Woods Second Section Exit (West)'), + ('Skull Woods Final Section', 'Skull Woods Final Section Exit'), + ('Ice Palace', 'Ice Palace Exit'), + ('Misery Mire', 'Misery Mire Exit'), + ('Palace of Darkness', 'Palace of Darkness Exit'), + ('Swamp Palace', 'Swamp Palace Exit'), # requires additional patch for flooding moat if moved + + ('Turtle Rock', 'Turtle Rock Exit (Front)'), + ('Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)'), + ('Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)'), + ('Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit') + ] + diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 54c58417..704754aa 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -5,6 +5,8 @@ import copy from collections import defaultdict, OrderedDict from BaseClasses import RegionType +from source.overworld.EntranceData import door_addresses + class EntrancePool(object): def __init__(self, world, player): @@ -200,7 +202,10 @@ def do_vanilla_connections(avail_pool): connect_vanilla_two_way(ent, avail_pool.default_map[ent], avail_pool) if ent in avail_pool.one_way_map and avail_pool.one_way_map[ent] in avail_pool.exits: connect_vanilla(ent, avail_pool.one_way_map[ent], avail_pool) - if avail_pool.inverted: + if avail_pool.world.is_bombshop_start(avail_pool.player): + ext = avail_pool.world.get_entrance('Big Bomb Shop Exit', avail_pool.player) + ext.connect(avail_pool.world.get_region('Big Bomb Shop Area', avail_pool.player)) + if avail_pool.world.is_dark_chapel_start(avail_pool.player): ext = avail_pool.world.get_entrance('Dark Sanctuary Hint Exit', avail_pool.player) ext.connect(avail_pool.world.get_region('Dark Chapel Area', avail_pool.player)) @@ -391,7 +396,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): def do_old_man_cave_exit(entrances, exits, avail, cross_world): if 'Old Man Cave Exit (East)' in exits: - from EntranceShuffle import build_accessible_region_list + from OverworldShuffle import build_accessible_region_list if not avail.world.is_tile_swapped(0x03, avail.player) or avail.world.shuffle[avail.player] == 'district': region_name = 'West Death Mountain (Top)' else: @@ -439,11 +444,11 @@ def do_blacksmith(entrances, exits, avail): links_region = links_region.name blacksmith_options = list(get_accessible_entrances(links_region, avail, assumed_inventory, False, True, True)) - if avail.inverted: + if avail.world.is_dark_chapel_start(avail.player): dark_sanc = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name blacksmith_options = list(OrderedDict.fromkeys(blacksmith_options + list(get_accessible_entrances(dark_sanc, avail, assumed_inventory, False, True, True)))) elif avail.is_sanc_forced_in_hc(): - sanc_region = avail.world.get_entrance('Sanctuary Exit',avail. player).connected_region + sanc_region = avail.world.get_entrance('Sanctuary Exit', avail.player).connected_region if sanc_region: blacksmith_options = list(OrderedDict.fromkeys(blacksmith_options + list(get_accessible_entrances(sanc_region.name, avail, assumed_inventory, False, True, True)))) else: @@ -582,7 +587,7 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world): def do_dark_sanc(entrances, exits, avail): - if avail.inverted: + if avail.world.is_dark_chapel_start(avail.player): ext = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player) if 'Dark Sanctuary Hint' in exits: forbidden = list(Isolated_LH_Doors) @@ -630,7 +635,7 @@ def do_links_house(entrances, exits, avail, cross_world): forbidden.append('Mimic Cave') if avail.world.is_bombshop_start(avail.player) and (avail.inverted == avail.world.is_tile_swapped(0x03, avail.player)): forbidden.extend(['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']) - if avail.inverted and avail.world.shuffle[avail.player] != 'district': + if avail.world.is_dark_chapel_start(avail.player) and avail.world.shuffle[avail.player] != 'district': dark_sanc_region = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name forbidden.extend(get_nearby_entrances(avail, dark_sanc_region)) else: @@ -669,7 +674,7 @@ def do_links_house(entrances, exits, avail, cross_world): avail.links_on_mountain = True # lobby shuffle means you ought to keep links house in the same world - sanc_spawn_can_be_dark = (not avail.inverted and avail.world.doorShuffle[avail.player] in ['partitioned', 'crossed'] + sanc_spawn_can_be_dark = (not avail.world.is_dark_chapel_start(avail.player) and avail.world.doorShuffle[avail.player] in ['partitioned', 'crossed'] and avail.world.intensity[avail.player] >= 3) if (cross_world and not sanc_spawn_can_be_dark) or avail.world.shuffle[avail.player] == 'district': @@ -824,8 +829,7 @@ def get_accessible_entrances(start_region, avail, assumed_inventory=[], cross_wo from Main import copy_world_premature from BaseClasses import CollectionState from Items import ItemFactory - from EntranceShuffle import build_accessible_region_list - from OverworldShuffle import one_way_ledges + from OverworldShuffle import build_accessible_region_list, one_way_ledges for p in range(1, avail.world.players + 1): avail.world.key_logic[p] = {} @@ -1340,18 +1344,24 @@ def do_limited_shuffle_exclude_drops(pool_def, avail, lw=True): def do_vanilla_connect(pool_def, avail): - if pool_def['condition'] == 'shopsanity': + if 'shopsanity' in pool_def['condition']: if avail.world.shopsanity[avail.player]: return - elif pool_def['condition'] == 'pottery': # this condition involves whether caves with pots are shuffled or not + if 'pottery' in pool_def['condition']: # this condition involves whether caves with pots are shuffled or not if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']: return - elif pool_def['condition'] == 'takeany': + if 'takeany' in pool_def['condition']: if avail.world.take_any[avail.player] == 'fixed': return - elif pool_def['condition'] == 'bonk': + if 'bonk' in pool_def['condition']: if avail.world.shuffle_bonk_drops[avail.player]: return + if 'dropshuffle' in pool_def['condition']: + if avail.world.dropshuffle[avail.player] not in ['none', 'keys']: + return + if 'enemy_drop' in pool_def['condition']: + if avail.world.dropshuffle[avail.player] not in ['none', 'keys'] or avail.world.enemy_shuffle[avail.player] != 'none': + return defaults = {**default_connections, **(inverted_default_connections if avail.inverted != avail.world.is_tile_swapped(0x1b, avail.player) else open_default_connections)} for entrance in pool_def['entrances']: if entrance in avail.entrances: @@ -1871,35 +1881,56 @@ modes = { 'special': 'vanilla', 'condition': '', 'entrances': ['Mire Fairy', 'Archery Game', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', - 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Shop', - 'East Dark World Hint', 'Kakariko Gamble Game', 'Long Fairy Cave', - 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', - 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', 'Bonk Fairy (Light)'], + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'East Dark World Hint', + 'Kakariko Gamble Game', 'Bush Covered House', 'Fortune Teller (Light)', + 'Lost Woods Gamble', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy'], }, 'fixed_shops': { 'special': 'vanilla', 'condition': 'shopsanity', - 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', - 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', - 'Lake Hylia Shop'], + 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', + 'Red Shield Shop', 'Kakariko Shop', 'Lake Hylia Shop', 'Dark Lake Hylia Shop'], }, 'fixed_takeanys': { 'special': 'vanilla', 'condition': 'takeany', - 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)'], + 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy'], + }, + 'fixed_takeanys_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': ['takeany', 'enemy_drop'], + 'entrances': ['Bonk Fairy (Dark)'], }, 'fixed_pottery': { 'special': 'vanilla', 'condition': 'pottery', 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', - 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint'] + '20 Rupee Cave', '50 Rupee Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Mire Hint'] + }, + 'fixed_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': 'enemy_drop', + 'entrances': ['Long Fairy Cave', 'Bonk Fairy (Light)'] + }, + 'fixed_pots_n_bones_fairies': { + 'special': 'vanilla', + 'condition': ['pottery', 'enemy_drop'], + 'entrances': ['Hookshot Fairy'] + }, + 'fixed_pots_n_bones': { + 'special': 'vanilla', + 'condition': ['pottery', 'dropshuffle'], + 'entrances': ['Light World Bomb Hut'] + }, + 'fixed_shop_n_bones': { + 'special': 'vanilla', + 'condition': ['shopsanity', 'enemy_drop'], + 'entrances': ['Capacity Upgrade'] }, 'fixed_bonk': { 'special': 'vanilla', - 'condition': 'bonk', + 'condition': ['enemy_drop', 'bonk'], 'entrances': ['Good Bee Cave'] }, 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps @@ -1908,15 +1939,15 @@ modes = { 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', - 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Good Bee Cave', + 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Dark Lake Hylia Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Lake Hylia Shop', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint', 'Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)', - 'Links House', 'Tavern North'] + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Desert Fairy', + 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy', + 'Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)', + 'Mire Hint', 'Links House', 'Tavern North'] }, 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything 'special': 'old_man_cave_east', @@ -1962,35 +1993,56 @@ modes = { 'special': 'vanilla', 'condition': '', 'entrances': ['Mire Fairy', 'Archery Game', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', - 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Shop', - 'East Dark World Hint', 'Kakariko Gamble Game', 'Long Fairy Cave', - 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', - 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', 'Bonk Fairy (Light)'], + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'East Dark World Hint', + 'Kakariko Gamble Game', 'Bush Covered House', 'Fortune Teller (Light)', + 'Lost Woods Gamble', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy'], }, 'fixed_shops': { 'special': 'vanilla', 'condition': 'shopsanity', - 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', - 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', - 'Lake Hylia Shop'], + 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', + 'Red Shield Shop', 'Kakariko Shop', 'Lake Hylia Shop', 'Dark Lake Hylia Shop'], }, 'fixed_takeanys': { 'special': 'vanilla', 'condition': 'takeany', - 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)'], + 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy'], + }, + 'fixed_takeanys_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': ['takeany', 'enemy_drop'], + 'entrances': ['Bonk Fairy (Dark)'], }, 'fixed_pottery': { 'special': 'vanilla', 'condition': 'pottery', 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', - 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint'] + '20 Rupee Cave', '50 Rupee Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Mire Hint'] + }, + 'fixed_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': 'enemy_drop', + 'entrances': ['Long Fairy Cave', 'Bonk Fairy (Light)'] + }, + 'fixed_pots_n_bones_fairies': { + 'special': 'vanilla', + 'condition': ['pottery', 'enemy_drop'], + 'entrances': ['Hookshot Fairy'] + }, + 'fixed_pots_n_bones': { + 'special': 'vanilla', + 'condition': ['pottery', 'dropshuffle'], + 'entrances': ['Light World Bomb Hut'] + }, + 'fixed_shop_n_bones': { + 'special': 'vanilla', + 'condition': ['shopsanity', 'enemy_drop'], + 'entrances': ['Capacity Upgrade'] }, 'fixed_bonk': { 'special': 'vanilla', - 'condition': 'bonk', + 'condition': ['enemy_drop', 'bonk'], 'entrances': ['Good Bee Cave'] }, 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps @@ -1999,15 +2051,15 @@ modes = { 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', - 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Good Bee Cave', + 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Dark Lake Hylia Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Lake Hylia Shop', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint', 'Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)', - 'Links House', 'Tavern North'] # inverted links house gets substituted + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Desert Fairy', + 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy', + 'Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)', + 'Mire Hint', 'Links House', 'Tavern North'] } } }, @@ -2575,6 +2627,8 @@ mandatory_connections = [('Lost Woods Hideout (top to bottom)', 'Lost Woods Hide ('Paradox Cave Push Block', 'Paradox Cave Front'), ('Paradox Cave Chest Area NE', 'Paradox Cave Bomb Area'), ('Paradox Cave Bomb Jump', 'Paradox Cave'), + ('Paradox Cave Climb', 'Paradox Cave (Top)'), + ('Paradox Cave Descent', 'Paradox Cave'), ('Paradox Cave Drop', 'Paradox Cave Chest Area'), ('Paradox Shop', 'Paradox Shop'), ('Fairy Ascension Cave Climb', 'Fairy Ascension Cave (Top)'), @@ -2762,155 +2816,6 @@ inverted_default_connections = {'Inverted Pyramid Hole': 'Pyramid', 'Pyramid Exit': 'Hyrule Castle Ledge', 'Inverted Pyramid Entrance': 'Bottom of Pyramid'} -# format: -# Key=Name -# addr = (door_index, exitdata) # multiexit -# | ([addr], None) # holes -# exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) - -# ToDo somehow merge this with creation of the locations -door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000), 0x00), - 'Desert Palace Entrance (South)': (0x08, (0x0084, 0x30, 0x0314, 0x0c56, 0x00a6, 0x0ca8, 0x0128, 0x0cc3, 0x0133, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), - 'Desert Palace Entrance (West)': (0x0A, (0x0083, 0x30, 0x0280, 0x0c46, 0x0003, 0x0c98, 0x0088, 0x0cb3, 0x0090, 0x0a, 0xfd, 0x0000, 0x0000), 0x00), - 'Desert Palace Entrance (North)': (0x0B, (0x0063, 0x30, 0x0016, 0x0c00, 0x00a2, 0x0c28, 0x0128, 0x0c6d, 0x012f, 0x00, 0x0e, 0x0000, 0x0000), 0x00), - 'Desert Palace Entrance (East)': (0x09, (0x0085, 0x30, 0x02a8, 0x0c4a, 0x0142, 0x0c98, 0x01c8, 0x0cb7, 0x01cf, 0x06, 0xfe, 0x0000, 0x0000), 0x00), - 'Eastern Palace': (0x07, (0x00c9, 0x1e, 0x005a, 0x0600, 0x0ed6, 0x0618, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000), 0x00), - 'Tower of Hera': (0x32, (0x0077, 0x03, 0x0050, 0x0014, 0x087c, 0x0068, 0x08f0, 0x0083, 0x08fb, 0x0a, 0xf4, 0x0000, 0x0000), 0x00), - 'Hyrule Castle Entrance (South)': (0x03, (0x0061, 0x1b, 0x0530, 0x0692, 0x0784, 0x06cc, 0x07f8, 0x06ff, 0x0803, 0x0e, 0xfa, 0x0000, 0x87be), 0x00), - 'Hyrule Castle Entrance (West)': (0x02, (0x0060, 0x1b, 0x0016, 0x0600, 0x06ae, 0x0604, 0x0728, 0x066d, 0x0733, 0x00, 0x02, 0x0000, 0x8124), 0x00), - 'Hyrule Castle Entrance (East)': (0x04, (0x0062, 0x1b, 0x004a, 0x0600, 0x0856, 0x0604, 0x08c8, 0x066d, 0x08d3, 0x00, 0xfa, 0x0000, 0x8158), 0x00), - 'Inverted Pyramid Entrance': (0x35, (0x0010, 0x1b, 0x000e, 0x0600, 0x0676, 0x0604, 0x06e8, 0x066d, 0x06f3, 0x00, 0x0a, 0x0000, 0x811c), 0x00), - 'Agahnims Tower': (0x23, (0x00e0, 0x1b, 0x0032, 0x0600, 0x0784, 0x0634, 0x07f8, 0x066d, 0x0803, 0x00, 0x0a, 0x0000, 0x82be), 0x40), - 'Thieves Town': (0x33, (0x00db, 0x58, 0x0b2e, 0x075a, 0x0176, 0x07a8, 0x01f8, 0x07c7, 0x0203, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Skull Woods First Section Door': (0x29, (0x0058, 0x40, 0x0f4c, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0xfe, 0x0000, 0x0000), 0x00), - 'Skull Woods Second Section Door (East)': (0x28, (0x0057, 0x40, 0x0eb8, 0x01e6, 0x01c2, 0x0238, 0x0248, 0x0253, 0x024f, 0x0a, 0xfe, 0x0000, 0x0000), 0x00), - 'Skull Woods Second Section Door (West)': (0x27, (0x0056, 0x40, 0x0c8e, 0x01a6, 0x0062, 0x01f8, 0x00e8, 0x0213, 0x00ef, 0x0a, 0x0e, 0x0000, 0x0000), 0x00), - 'Skull Woods Final Section': (0x2A, (0x0059, 0x40, 0x0282, 0x0066, 0x0016, 0x00b8, 0x0098, 0x00d3, 0x00a3, 0x0a, 0xfa, 0x0000, 0x0000), 0x20), - 'Ice Palace': (0x2C, (0x000e, 0x75, 0x0bc6, 0x0d6a, 0x0c3e, 0x0db8, 0x0cb8, 0x0dd7, 0x0cc3, 0x06, 0xf2, 0x0000, 0x0000), 0x00), - 'Misery Mire': (0x26, (0x0098, 0x70, 0x0414, 0x0c79, 0x00a6, 0x0cc7, 0x0128, 0x0ce6, 0x0133, 0x07, 0xfa, 0x0000, 0x0000), 0x20), - 'Palace of Darkness': (0x25, (0x004a, 0x5e, 0x005a, 0x0600, 0x0ed6, 0x0628, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000), 0x20), - 'Swamp Palace': (0x24, (0x0028, 0x7b, 0x049e, 0x0e8c, 0x06f2, 0x0ed8, 0x0778, 0x0ef9, 0x077f, 0x04, 0xfe, 0x0000, 0x0000), 0x00), - 'Turtle Rock': (0x34, (0x00d6, 0x47, 0x0712, 0x00da, 0x0e96, 0x0128, 0x0f08, 0x0147, 0x0f13, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Dark Death Mountain Ledge (West)': (0x14, (0x0023, 0x45, 0x07ca, 0x0103, 0x0c46, 0x0157, 0x0cb8, 0x0172, 0x0cc3, 0x0b, 0x0a, 0x0000, 0x0000), 0x00), - 'Dark Death Mountain Ledge (East)': (0x18, (0x0024, 0x45, 0x07e0, 0x0103, 0x0d00, 0x0157, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Turtle Rock Isolated Ledge Entrance': (0x17, (0x00d5, 0x45, 0x0ad4, 0x0164, 0x0ca6, 0x01b8, 0x0d18, 0x01d3, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), - 'Hyrule Castle Secret Entrance Stairs': (0x31, (0x0055, 0x1b, 0x044a, 0x067a, 0x0854, 0x06c8, 0x08c8, 0x06e7, 0x08d3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Kakariko Well Cave': (0x38, (0x002f, 0x18, 0x0386, 0x0665, 0x0032, 0x06b7, 0x00b8, 0x06d2, 0x00bf, 0x0b, 0xfe, 0x0000, 0x0000), 0x00), - 'Bat Cave Cave': (0x10, (0x00e3, 0x22, 0x0412, 0x087a, 0x048e, 0x08c8, 0x0508, 0x08e7, 0x0513, 0x06, 0x02, 0x0000, 0x0000), 0x00), - 'Elder House (East)': (0x0D, (0x00f3, 0x18, 0x02c4, 0x064a, 0x0222, 0x0698, 0x02a8, 0x06b7, 0x02af, 0x06, 0xfe, 0x05d4, 0x0000), 0x00), - 'Elder House (West)': (0x0C, (0x00f2, 0x18, 0x02bc, 0x064c, 0x01e2, 0x0698, 0x0268, 0x06b9, 0x026f, 0x04, 0xfe, 0x05cc, 0x0000), 0x00), - 'North Fairy Cave': (0x37, (0x0008, 0x15, 0x0088, 0x0400, 0x0a36, 0x0448, 0x0aa8, 0x046f, 0x0ab3, 0x00, 0x0a, 0x0000, 0x0000), 0x00), - 'Lost Woods Hideout Stump': (0x2B, (0x00e1, 0x00, 0x0f4e, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0x0e, 0x0000, 0x0000), 0x00), - 'Lumberjack Tree Cave': (0x11, (0x00e2, 0x02, 0x0118, 0x0015, 0x04c6, 0x0067, 0x0548, 0x0082, 0x0553, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), - 'Two Brothers House (East)': (0x0F, (0x00f5, 0x29, 0x0880, 0x0b07, 0x0200, 0x0b58, 0x0238, 0x0b74, 0x028d, 0x09, 0x00, 0x0b86, 0x0000), 0x00), - 'Two Brothers House (West)': (0x0E, (0x00f4, 0x28, 0x08a0, 0x0b06, 0x0100, 0x0b58, 0x01b8, 0x0b73, 0x018d, 0x0a, 0x00, 0x0bb6, 0x0000), 0x00), - 'Sanctuary': (0x01, (0x0012, 0x13, 0x001c, 0x0400, 0x06de, 0x0414, 0x0758, 0x046d, 0x0763, 0x00, 0x02, 0x0000, 0x01aa), 0x00), - 'Old Man Cave (West)': (0x05, (0x00f0, 0x0a, 0x03a0, 0x0264, 0x0500, 0x02b8, 0x05a8, 0x02d3, 0x058d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Old Man Cave (East)': (0x06, (0x00f1, 0x03, 0x1402, 0x0294, 0x0604, 0x02e8, 0x0678, 0x0303, 0x0683, 0x0a, 0xfc, 0x0000, 0x0000), 0x00), - 'Old Man House (Bottom)': (0x2F, (0x00e4, 0x03, 0x181a, 0x031e, 0x06b4, 0x03a7, 0x0728, 0x038d, 0x0733, 0x00, 0x0c, 0x0000, 0x0000), 0x00), - 'Old Man House (Top)': (0x30, (0x00e5, 0x03, 0x10c6, 0x0224, 0x0814, 0x0278, 0x0888, 0x0293, 0x0893, 0x0a, 0x0c, 0x0000, 0x0000), 0x00), - 'Death Mountain Return Cave (East)': (0x2E, (0x00e7, 0x03, 0x0d82, 0x01c4, 0x0600, 0x0218, 0x0648, 0x0233, 0x067f, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Death Mountain Return Cave (West)': (0x2D, (0x00e6, 0x0a, 0x00a0, 0x0205, 0x0500, 0x0257, 0x05b8, 0x0272, 0x058d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Spectacle Rock Cave Peak': (0x22, (0x00ea, 0x03, 0x092c, 0x0133, 0x0754, 0x0187, 0x07c8, 0x01a2, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), - 'Spectacle Rock Cave': (0x21, (0x00fa, 0x03, 0x0eac, 0x01e3, 0x0754, 0x0237, 0x07c8, 0x0252, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), - 'Spectacle Rock Cave (Bottom)': (0x20, (0x00f9, 0x03, 0x0d9c, 0x01c3, 0x06d4, 0x0217, 0x0748, 0x0232, 0x0753, 0x0b, 0xfc, 0x0000, 0x0000), 0x00), - 'Paradox Cave (Bottom)': (0x1D, (0x00ff, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0237, 0x0da8, 0x0252, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Paradox Cave (Middle)': (0x1E, (0x00ef, 0x05, 0x17e0, 0x0304, 0x0d00, 0x0358, 0x0dc8, 0x0373, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Paradox Cave (Top)': (0x1F, (0x00df, 0x05, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Fairy Ascension Cave (Bottom)': (0x19, (0x00fd, 0x05, 0x0dd4, 0x01c4, 0x0ca6, 0x0218, 0x0d18, 0x0233, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000), 0x00), - 'Fairy Ascension Cave (Top)': (0x1A, (0x00ed, 0x05, 0x0ad4, 0x0163, 0x0ca6, 0x01b7, 0x0d18, 0x01d2, 0x0d23, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), - 'Spiral Cave': (0x1C, (0x00ee, 0x05, 0x07c8, 0x0108, 0x0c46, 0x0158, 0x0cb8, 0x0177, 0x0cc3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Spiral Cave (Bottom)': (0x1B, (0x00fe, 0x05, 0x0cca, 0x01a3, 0x0c56, 0x01f7, 0x0cc8, 0x0212, 0x0cd3, 0x0b, 0xfa, 0x0000, 0x0000), 0x00), - 'Bumper Cave (Bottom)': (0x15, (0x00fb, 0x4a, 0x03a0, 0x0263, 0x0500, 0x02b7, 0x05a8, 0x02d2, 0x058d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Bumper Cave (Top)': (0x16, (0x00eb, 0x4a, 0x00a0, 0x020a, 0x0500, 0x0258, 0x05b8, 0x0277, 0x058d, 0x06, 0x00, 0x0000, 0x0000), 0x00), - 'Superbunny Cave (Top)': (0x13, (0x00e8, 0x45, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000), 0x00), - 'Superbunny Cave (Bottom)': (0x12, (0x00f8, 0x45, 0x0ee0, 0x01e4, 0x0d00, 0x0238, 0x0d78, 0x0253, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000), 0x00), - 'Hookshot Cave': (0x39, (0x003c, 0x45, 0x04da, 0x00a3, 0x0cd6, 0x0107, 0x0d48, 0x0112, 0x0d53, 0x0b, 0xfa, 0x0000, 0x0000), 0x20), - 'Hookshot Cave Back Entrance': (0x3A, (0x002c, 0x45, 0x004c, 0x0000, 0x0c56, 0x0038, 0x0cc8, 0x006f, 0x0cd3, 0x00, 0x0a, 0x0000, 0x0000), 0x00), - 'Ganons Tower': (0x36, (0x000c, 0x43, 0x0052, 0x0000, 0x0884, 0x0028, 0x08f8, 0x006f, 0x0903, 0x00, 0xfc, 0x0000, 0x0000), 0x20), - 'Pyramid Entrance': (0x35, (0x0010, 0x5b, 0x0b0e, 0x075a, 0x0674, 0x07a8, 0x06e8, 0x07c7, 0x06f3, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Skull Woods First Section Hole (West)': ([0xDB84D, 0xDB84E], None), - 'Skull Woods First Section Hole (East)': ([0xDB84F, 0xDB850], None), - 'Skull Woods First Section Hole (North)': ([0xDB84C], None), - 'Skull Woods Second Section Hole': ([0xDB851, 0xDB852], None), - 'Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856], None), - 'Inverted Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856, 0x180340], None), - 'Waterfall of Wishing': (0x5B, (0x0114, 0x0f, 0x0080, 0x0200, 0x0e00, 0x0207, 0x0e60, 0x026f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Dam': (0x4D, (0x010b, 0x3b, 0x04a0, 0x0e8a, 0x06fa, 0x0ed8, 0x0778, 0x0ef7, 0x077f, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Blinds Hideout': (0x60, (0x0119, 0x18, 0x02b2, 0x064a, 0x0186, 0x0697, 0x0208, 0x06b7, 0x0213, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Hyrule Castle Secret Entrance Drop': ([0xDB858], None), - 'Bonk Fairy (Light)': (0x76, (0x0126, 0x2b, 0x00a0, 0x0a0a, 0x0700, 0x0a67, 0x0788, 0x0a77, 0x0785, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Lake Hylia Fairy': (0x5D, (0x0115, 0x2e, 0x0016, 0x0a00, 0x0cb6, 0x0a37, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), - 'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000), 0x20), - 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), - 'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000), 0x00), - 'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000), 0x00), - 'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Lake Hylia Shop': (0x57, (0x0112, 0x35, 0x0022, 0x0c00, 0x0b1a, 0x0c26, 0x0b98, 0x0c6d, 0x0b9f, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Capacity Upgrade': (0x5C, (0x0115, 0x35, 0x0a46, 0x0d36, 0x0c2a, 0x0d88, 0x0ca8, 0x0da3, 0x0caf, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Kakariko Well Drop': ([0xDB85C, 0xDB85D], None), - 'Blacksmiths Hut': (0x63, (0x0121, 0x22, 0x010c, 0x081a, 0x0466, 0x0868, 0x04d8, 0x0887, 0x04e3, 0x06, 0xfa, 0x041A, 0x0000), 0x00), - 'Bat Cave Drop': ([0xDB859, 0xDB85A], None), - 'Sick Kids House': (0x3F, (0x0102, 0x18, 0x10be, 0x0826, 0x01f6, 0x0877, 0x0278, 0x0893, 0x0283, 0x08, 0xf8, 0x14CE, 0x0000), 0x00), - 'North Fairy Cave Drop': ([0xDB857], None), - 'Lost Woods Gamble': (0x3B, (0x0100, 0x00, 0x004e, 0x0000, 0x0272, 0x0008, 0x02f0, 0x006f, 0x02f7, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Fortune Teller (Light)': (0x64, (0x0122, 0x11, 0x060e, 0x04b4, 0x027d, 0x0508, 0x02f8, 0x0523, 0x0302, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Snitch Lady (East)': (0x3D, (0x0101, 0x18, 0x0ad8, 0x074a, 0x02c6, 0x0798, 0x0348, 0x07b7, 0x0353, 0x06, 0xfa, 0x0DE8, 0x0000), 0x00), - 'Snitch Lady (West)': (0x3E, (0x0101, 0x18, 0x0788, 0x0706, 0x0046, 0x0758, 0x00c8, 0x0773, 0x00d3, 0x08, 0xf8, 0x0B98, 0x0000), 0x00), - 'Bush Covered House': (0x43, (0x0103, 0x18, 0x1156, 0x081a, 0x02b6, 0x0868, 0x0338, 0x0887, 0x0343, 0x06, 0xfa, 0x1466, 0x0000), 0x00), - 'Tavern (Front)': (0x41, (0x0103, 0x18, 0x1842, 0x0916, 0x0206, 0x0967, 0x0288, 0x0983, 0x0293, 0x08, 0xf8, 0x1C50, 0x0000), 0x00), - 'Light World Bomb Hut': (0x49, (0x0107, 0x18, 0x1800, 0x0916, 0x0000, 0x0967, 0x0068, 0x0983, 0x008d, 0x08, 0xf8, 0x9C0C, 0x0000), 0x02), - 'Kakariko Shop': (0x45, (0x011f, 0x18, 0x16a8, 0x08e7, 0x0136, 0x0937, 0x01b8, 0x0954, 0x01c3, 0x07, 0xf9, 0x1AB6, 0x0000), 0x00), - 'Lost Woods Hideout Drop': ([0xDB853], None), - 'Lumberjack Tree Tree': ([0xDB85B], None), - 'Cave 45': (0x50, (0x011b, 0x32, 0x0680, 0x0cc9, 0x0400, 0x0d16, 0x0438, 0x0d36, 0x0485, 0x07, 0xf9, 0x0000, 0x0000), 0x00), - 'Graveyard Cave': (0x51, (0x011b, 0x14, 0x0016, 0x0400, 0x08a2, 0x0446, 0x0918, 0x046d, 0x091f, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Checkerboard Cave': (0x7D, (0x0126, 0x30, 0x00c8, 0x0c0a, 0x024a, 0x0c67, 0x02c8, 0x0c77, 0x02cf, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Mini Moldorm Cave': (0x7C, (0x0123, 0x35, 0x1480, 0x0e96, 0x0a00, 0x0ee8, 0x0a68, 0x0f03, 0x0a85, 0x08, 0xf8, 0x0000, 0x0000), 0x02), - 'Long Fairy Cave': (0x54, (0x011e, 0x2f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Good Bee Cave': (0x6A, (0x0120, 0x37, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000), 0x00), - '20 Rupee Cave': (0x7A, (0x0125, 0x37, 0x0200, 0x0c23, 0x0e00, 0x0c86, 0x0e68, 0x0c92, 0x0e7d, 0x0d, 0xf3, 0x0000, 0x0000), 0x20), - '50 Rupee Cave': (0x78, (0x0124, 0x3a, 0x0790, 0x0eea, 0x047a, 0x0f47, 0x04f8, 0x0f57, 0x04ff, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Ice Rod Cave': (0x7F, (0x0120, 0x37, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x02), - 'Bonk Rock Cave': (0x79, (0x0124, 0x13, 0x0280, 0x044a, 0x0600, 0x04a7, 0x0638, 0x04b7, 0x067d, 0x06, 0xfa, 0x0000, 0x0000), 0x20), - 'Library': (0x48, (0x0107, 0x29, 0x0100, 0x0a14, 0x0200, 0x0a67, 0x0278, 0x0a83, 0x0285, 0x0a, 0xf6, 0x040E, 0x0000), 0x00), - 'Potion Shop': (0x4B, (0x0109, 0x16, 0x070a, 0x04e6, 0x0c56, 0x0538, 0x0cc8, 0x0553, 0x0cd3, 0x08, 0xf8, 0x0A98, 0x0000), 0x00), - 'Sanctuary Grave': ([0xDB85E], None), - 'Hookshot Fairy': (0x4F, (0x010c, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0d78, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), - 'Pyramid Fairy': (0x62, (0x0116, 0x5b, 0x0b1e, 0x0754, 0x06fa, 0x07a7, 0x0778, 0x07c3, 0x077f, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), - 'East Dark World Hint': (0x68, (0x010e, 0x6f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Palace of Darkness Hint': (0x67, (0x011a, 0x5e, 0x0c24, 0x0794, 0x0d12, 0x07e8, 0x0d90, 0x0803, 0x0d97, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Lake Hylia Fairy': (0x6C, (0x0115, 0x6e, 0x0016, 0x0a00, 0x0cb6, 0x0a36, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Dark Lake Hylia Ledge Fairy': (0x80, (0x0115, 0x77, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000), 0x02), - 'Dark Lake Hylia Ledge Spike Cave': (0x7B, (0x0125, 0x77, 0x0200, 0x0c27, 0x0e00, 0x0c86, 0x0e68, 0x0c96, 0x0e7d, 0x09, 0xf7, 0x0000, 0x0000), 0x20), - 'Dark Lake Hylia Ledge Hint': (0x69, (0x010e, 0x77, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Hype Cave': (0x3C, (0x011e, 0x74, 0x00a0, 0x0c0a, 0x0900, 0x0c58, 0x0988, 0x0c77, 0x097d, 0x06, 0xfa, 0x0000, 0x0000), 0x02), - 'Bonk Fairy (Dark)': (0x77, (0x0126, 0x6b, 0x00a0, 0x0a05, 0x0700, 0x0a66, 0x0788, 0x0a72, 0x0785, 0x0b, 0xf5, 0x0000, 0x0000), 0x20), - 'Brewery': (0x47, (0x0106, 0x58, 0x16a8, 0x08e4, 0x013e, 0x0938, 0x01b8, 0x0953, 0x01c3, 0x0a, 0xf6, 0x1AB6, 0x0000), 0x02), - 'C-Shaped House': (0x53, (0x011c, 0x58, 0x09d8, 0x0744, 0x02ce, 0x0797, 0x0348, 0x07b3, 0x0353, 0x0a, 0xf6, 0x0DE8, 0x0000), 0x00), - 'Chest Game': (0x46, (0x0106, 0x58, 0x078a, 0x0705, 0x004e, 0x0758, 0x00c8, 0x0774, 0x00d3, 0x09, 0xf7, 0x0B98, 0x0000), 0x00), - 'Hammer Peg Cave': (0x7E, (0x0127, 0x62, 0x0894, 0x091e, 0x0492, 0x09a6, 0x0508, 0x098b, 0x050f, 0x00, 0x00, 0x0000, 0x0000), 0x20), - 'Red Shield Shop': (0x74, (0x0110, 0x5a, 0x079a, 0x06e8, 0x04d6, 0x0738, 0x0548, 0x0755, 0x0553, 0x08, 0xf8, 0x0AA8, 0x0000), 0x00), - 'Dark Sanctuary Hint': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000), 0x00), - 'Fortune Teller (Dark)': (0x65, (0x0122, 0x51, 0x0610, 0x04b4, 0x027e, 0x0507, 0x02f8, 0x0523, 0x0303, 0x0a, 0xf6, 0x091E, 0x0000), 0x00), - 'Dark World Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Lumberjack Shop': (0x56, (0x010f, 0x42, 0x041c, 0x0074, 0x04e2, 0x00c7, 0x0558, 0x00e3, 0x055f, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Potion Shop': (0x6E, (0x010f, 0x56, 0x080e, 0x04f4, 0x0c66, 0x0548, 0x0cd8, 0x0563, 0x0ce3, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Archery Game': (0x58, (0x0111, 0x69, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00), - 'Mire Shed': (0x5E, (0x010d, 0x70, 0x0384, 0x0c69, 0x001e, 0x0cb6, 0x0098, 0x0cd6, 0x00a3, 0x07, 0xf9, 0x0000, 0x0000), 0x00), - 'Mire Hint': (0x61, (0x0114, 0x70, 0x0654, 0x0cc5, 0x02aa, 0x0d16, 0x0328, 0x0d32, 0x032f, 0x09, 0xf7, 0x0000, 0x0000), 0x00), - 'Mire Fairy': (0x55, (0x0115, 0x70, 0x03a8, 0x0c6a, 0x013a, 0x0cb7, 0x01b8, 0x0cd7, 0x01bf, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Spike Cave': (0x40, (0x0117, 0x43, 0x0ed4, 0x01e4, 0x08aa, 0x0236, 0x0928, 0x0253, 0x092f, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Dark Death Mountain Shop': (0x6D, (0x0112, 0x45, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0da8, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), - 'Dark Death Mountain Fairy': (0x6F, (0x0115, 0x43, 0x1400, 0x0294, 0x0600, 0x02e8, 0x0678, 0x0303, 0x0685, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), - 'Mimic Cave': (0x4E, (0x010c, 0x05, 0x07e0, 0x0103, 0x0d00, 0x0156, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000), 0x00), - 'Big Bomb Shop': (0x52, (0x011c, 0x6c, 0x0506, 0x0a9a, 0x0832, 0x0ae7, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfa, 0x0816, 0x0000), 0x00), - 'Dark Lake Hylia Shop': (0x73, (0x010f, 0x75, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Lumberjack House': (0x75, (0x011f, 0x02, 0x049c, 0x0088, 0x04e6, 0x00d8, 0x0558, 0x00f7, 0x0563, 0x08, 0xf8, 0x07AA, 0x0000), 0x00), - 'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), - 'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)} # format: # Key=Name diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index 69b570a1..f082a4f5 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -94,6 +94,7 @@ class DataTables: # _00FA81 is LW normal # _00FAC1 is LW post-aga # _00FB01 is DW + # _00FA41 is rain state self.write_ow_sprite_data_to_rom(rom) for sprite, stats in self.enemy_stats.items(): # write health to rom @@ -134,9 +135,11 @@ class DataTables: # calculate how big this table is going to be? # bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 # ending_byte = 0x09CB3B + bytes - max_per_state = {0: 0x40, 1: 0x90, 2: 0x8D} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla) + max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard) + pointer_address = snes_to_pc(0x09C881) - data_pointer = snes_to_pc(0x09CB3B) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla) + # currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard) + data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard) empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_byte(data_pointer, 0xff) cached_dark_world = {} diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index c37ea0a3..77f6dd2e 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -106,6 +106,7 @@ def roll_settings(weights): if 'u' in dungeon_items: ret.keyshuffle = 'universal' ret.bigkeyshuffle = get_choice_bool('bigkey_shuffle') if 'bigkey_shuffle' in weights else 'b' in dungeon_items + ret.prizeshuffle = get_choice('prize_shuffle') ret.accessibility = get_choice('accessibility') ret.restrict_boss_items = get_choice('restrict_boss_items') diff --git a/test/MysteryTestSuite.py b/test/MysteryTestSuite.py index ea155dd8..e14baa61 100644 --- a/test/MysteryTestSuite.py +++ b/test/MysteryTestSuite.py @@ -1,3 +1,4 @@ +import os import subprocess import sys import multiprocessing @@ -8,6 +9,16 @@ from collections import OrderedDict cpu_threads = multiprocessing.cpu_count() py_version = f"{sys.version_info.major}.{sys.version_info.minor}" +PYLINE = "python" +PIPLINE_PATH = os.path.join(".","resources","user","meta","manifests","pipline.txt") +if os.path.isfile(PIPLINE_PATH): + with open(PIPLINE_PATH) as pipline_file: + PYLINE = pipline_file.read().replace("-m pip","").strip() + +results = { + "errors": [], + "success": [] +} def main(args=None): successes = [] @@ -25,7 +36,7 @@ def main(args=None): def test(testname: str, command: str): tests[testname] = [command] - basecommand = f"python3.8 Mystery.py --suppress_rom --suppress_meta" + basecommand = f"{PYLINE} Mystery.py --suppress_rom --suppress_meta" def gen_seed(): taskcommand = basecommand + " " + command @@ -98,6 +109,10 @@ if __name__ == "__main__": cpu_threads = args.cpu_threads + LOGPATH = os.path.join(".","logs") + if not os.path.isdir(LOGPATH): + os.makedirs(LOGPATH) + for dr in [['mystery', args.count if args.count else 1, 1]]: for tense in range(1, dr[2] + 1): @@ -112,13 +127,36 @@ if __name__ == "__main__": print() if errors: - with open(f"{dr[0]}{(f'-{tense}' if dr[0] in ['basic', 'crossed'] else '')}-errors.txt", 'w') as stream: + errors_filename = f"{dr[0]}" + if dr[0] in ["basic","crossed"]: + errors_filename += f"-{tense}" + errors_filename += "-errors.txt" + with open( + os.path.join( + LOGPATH, + errors_filename + ), + 'w' + ) as stream: for error in errors: stream.write(error[0] + "\n") stream.write(error[1] + "\n") stream.write(error[2] + "\n\n") + error[2] = error[2].split("\n") + results["errors"].append(error) - with open("success.txt", "w") as stream: + with open(os.path.join(LOGPATH, "mystery-success.txt"), "w") as stream: stream.write(str.join("\n", successes)) + results["success"] = successes - input("Press enter to continue") + num_errors = len(results["errors"]) + num_success = len(results["success"]) + num_total = num_errors + num_success + + print(f"Errors: {num_errors}/{num_total}") + print(f"Success: {num_success}/{num_total}") + # print(results) + + if (num_errors/num_total) > (num_success/num_total): + # exit(1) + pass diff --git a/test/NewTestSuite.py b/test/NewTestSuite.py index 8e7b9e1c..b389795d 100644 --- a/test/NewTestSuite.py +++ b/test/NewTestSuite.py @@ -10,6 +10,16 @@ from collections import OrderedDict cpu_threads = multiprocessing.cpu_count() py_version = f"{sys.version_info.major}.{sys.version_info.minor}" +PYLINE = "python" +PIPLINE_PATH = os.path.join(".","resources","user","meta","manifests","pipline.txt") +if os.path.isfile(PIPLINE_PATH): + with open(PIPLINE_PATH) as pipline_file: + PYLINE = pipline_file.read().replace("-m pip","").strip() + +results = { + "errors": [], + "success": [] +} def main(args=None): successes = [] @@ -28,7 +38,7 @@ def main(args=None): def test(test_name: str, command: str, test_file: str): tests[test_name] = [command] - base_command = f"python3 DungeonRandomizer.py --suppress_rom --suppress_spoiler" + base_command = f"{PYLINE} DungeonRandomizer.py --suppress_rom --jsonout --spoiler none" def gen_seed(): task_command = base_command + " " + command @@ -102,7 +112,7 @@ if __name__ == "__main__": test_suites = {} # not sure if it supports subdirectories properly yet - for root, dirnames, filenames in os.walk('test/suite'): + for root, dirnames, filenames in os.walk(os.path.join("test","suite")): test_suites[root] = fnmatch.filter(filenames, '*.yaml') args = argparse.Namespace() @@ -113,14 +123,30 @@ if __name__ == "__main__": successes += s print() + LOGPATH = os.path.join(".","logs") + if not os.path.isdir(LOGPATH): + os.makedirs(LOGPATH) + if errors: - with open(f"new-test-suite-errors.txt", 'w') as stream: + with open(os.path.join(LOGPATH, "new-test-suite-errors.txt"), 'w') as stream: for error in errors: stream.write(error[0] + "\n") stream.write(error[1] + "\n") stream.write(error[2] + "\n\n") + error[2] = error[2].split("\n") + results["errors"].append(error) with open("new-test-suite-success.txt", "w") as stream: stream.write(str.join("\n", successes)) + results["success"] = successes - input("Press enter to continue") + num_errors = len(results["errors"]) + num_success = len(results["success"]) + num_total = num_errors + num_success + + print(f"Errors: {num_errors}/{num_total}") + print(f"Success: {num_success}/{num_total}") + # print(results) + + if (num_errors/num_total) > (num_success/num_total): + exit(1) diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index 78cf4e57..1db87e39 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -2,11 +2,11 @@ import unittest from BaseClasses import World, CollectionState from Dungeons import create_dungeons, get_dungeon_item_pool -from EntranceShuffle import mandatory_connections, connect_simple from ItemList import difficulties, generate_itempool from Items import ItemFactory from Regions import create_regions from Rules import set_rules +from source.overworld.EntranceShuffle2 import mandatory_connections, connect_simple class TestDungeon(unittest.TestCase): diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index f4f6e3d9..70e3fd6b 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -3,12 +3,13 @@ from DoorShuffle import link_doors from Doors import create_doors from Dungeons import create_dungeons, get_dungeon_item_pool from OverworldShuffle import link_overworld -from EntranceShuffle import link_entrances from ItemList import generate_itempool, difficulties from Items import ItemFactory from Regions import create_regions, mark_light_dark_world_regions, create_dungeon_regions, create_shops from RoomData import create_rooms from Rules import set_rules + +from source.overworld.EntranceShuffle2 import link_entrances_new from test.TestBase import TestBase @@ -25,7 +26,7 @@ class TestInverted(TestBase): create_rooms(self.world, 1) create_dungeons(self.world, 1) link_overworld(self.world, 1) - link_entrances(self.world, 1) + link_entrances_new(self.world, 1) link_doors(self.world, 1) generate_itempool(self.world, 1) self.world.required_medallions[1] = ['Ether', 'Quake'] diff --git a/test/inverted/TestInvertedBombRules.py b/test/inverted/TestInvertedBombRules.py index 974ae797..c3a94ff8 100644 --- a/test/inverted/TestInvertedBombRules.py +++ b/test/inverted/TestInvertedBombRules.py @@ -2,7 +2,8 @@ import unittest from BaseClasses import World from Dungeons import create_dungeons -from EntranceShuffle import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \ +# todo: this test needs to be rewritten unfortunately +from source.overworld.EntranceShuffle2 import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \ Inverted_LW_Entrances_Must_Exit, Inverted_LW_Dungeon_Entrances_Must_Exit, Inverted_Bomb_Shop_Multi_Cave_Doors, Inverted_Bomb_Shop_Single_Cave_Doors, Inverted_Blacksmith_Single_Cave_Doors, Inverted_Blacksmith_Multi_Cave_Doors from Regions import create_regions from ItemList import difficulties diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index cfdf3a3d..d0b0ba6b 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -3,7 +3,7 @@ from DoorShuffle import link_doors from Doors import create_doors from Dungeons import create_dungeons, get_dungeon_item_pool from OverworldShuffle import link_overworld -from EntranceShuffle import link_entrances +from source.overworld.EntranceShuffle2 import link_entrances_new from ItemList import generate_itempool, difficulties from Items import ItemFactory from OverworldGlitchRules import create_owg_connections @@ -27,7 +27,7 @@ class TestInvertedOWG(TestBase): create_dungeons(self.world, 1) link_overworld(self.world, 1) create_owg_connections(self.world, 1) - link_entrances(self.world, 1) + link_entrances_new(self.world, 1) link_doors(self.world, 1) generate_itempool(self.world, 1) self.world.required_medallions[1] = ['Ether', 'Quake'] diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index c114f24b..989cf64e 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -3,7 +3,7 @@ from DoorShuffle import link_doors from Doors import create_doors from Dungeons import create_dungeons, get_dungeon_item_pool from OverworldShuffle import link_overworld -from EntranceShuffle import link_entrances +from source.overworld.EntranceShuffle2 import link_entrances_new from ItemList import difficulties, generate_itempool from Items import ItemFactory from OverworldGlitchRules import create_owg_connections @@ -26,7 +26,7 @@ class TestVanillaOWG(TestBase): create_rooms(self.world, 1) create_dungeons(self.world, 1) link_overworld(self.world, 1) - link_entrances(self.world, 1) + link_entrances_new(self.world, 1) link_doors(self.world, 1) create_owg_connections(self.world, 1) generate_itempool(self.world, 1) diff --git a/test/stats/EntranceShuffleStats.py b/test/stats/EntranceShuffleStats.py index 9217a6ba..53b2e7e5 100644 --- a/test/stats/EntranceShuffleStats.py +++ b/test/stats/EntranceShuffleStats.py @@ -7,11 +7,13 @@ import time from collections import Counter, defaultdict from source.overworld.EntranceShuffle2 import link_entrances_new -from EntranceShuffle import link_entrances +# from source.oEntranceShuffle import link_entrances_new from BaseClasses import World from Regions import create_regions, create_dungeon_regions +# probably deprecated + # tested: open + crossed (lh) Mar. 17 (made changes) # tested: open + simple (lh) Mar. 22 diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index 6ed6e611..91ec449b 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -3,7 +3,7 @@ from DoorShuffle import link_doors from Doors import create_doors from Dungeons import create_dungeons, get_dungeon_item_pool from OverworldShuffle import link_overworld -from EntranceShuffle import link_entrances +from source.overworld.EntranceShuffle2 import link_entrances_new from ItemList import difficulties, generate_itempool from Items import ItemFactory from Regions import create_regions, create_dungeon_regions, create_shops, mark_light_dark_world_regions @@ -25,7 +25,7 @@ class TestVanilla(TestBase): create_rooms(self.world, 1) create_dungeons(self.world, 1) link_overworld(self.world, 1) - link_entrances(self.world, 1) + link_entrances_new(self.world, 1) link_doors(self.world, 1) generate_itempool(self.world, 1) self.world.required_medallions[1] = ['Ether', 'Quake']