From 59e875dc6d45c6aa266515128aa7872fcb8bb334 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 26 Jul 2021 11:28:45 -0600 Subject: [PATCH 001/293] updated readme and version --- Main.py | 2 +- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Main.py b/Main.py index 8909301a..e92bbf61 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0.12u' +__version__ = '0.4.0-dev' class EnemizerError(RuntimeError): diff --git a/README.md b/README.md index 31ff824b..e997d70d 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ Alternatively, run ```Gui.py``` for a simple graphical user interface. # Commonly Missed Things and Differences from other Randomizers +Most of these apply only when the door shuffle is not vanilla. + ### Starting Item -You start with a “Mirror Scroll”, a dumbed-down mirror that only works in dungeons, not the overworld and can’t erase blocks like the Mirror +You start with a “Mirror Scroll”, a dumbed-down mirror that only works in dungeons, not the overworld and can’t erase blocks like the Mirror. ### Navigation @@ -58,7 +60,7 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du ### Boss Differences -* You have to find the attic floor and bomb it open and bring the maiden to the light to fight Blind. In cross dungeon door shuffle, the attic can be in any dungeon. If hints are on, there is a special one about a cracked floor. +* You have to find the attic floor and bomb it open and bring the maiden to the light to fight Blind. In cross dungeon door shuffle, the attic can be in any dungeon. If you bring the maiden to the boss arena, she will hint were the cracked floor can be found. If hints are on, there is a special one about the cracked floor. * GT Bosses do not respawn after killing them in this mode. * Enemizer change: The attic/maiden sequence is now active and required when Blind is the boss of Theives' Town even when bosses are shuffled. @@ -70,7 +72,7 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du ### Misc -* Compass counts no longer function after you get the Triforce +* Compass counts no longer function after you get the Triforce (this is actually true in all randomizers) # Settings @@ -124,7 +126,7 @@ The rooms are left alone and it is up to the discretion of the player whether to #### Force -The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game. +The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game.cause then you would need time to check the map in a d ### Standardize Palettes (--standardize_palettes) No effect if door shuffle is not on crossed @@ -239,6 +241,37 @@ Arrow Capacity upgrades are now replaced by Rupees wherever it might end up. The Ten Arrows and 5 randomly selected Small Hearts or Blue Shields are replaced by the quiver item (represented by the Single Arrow in game.) 5 Red Potion refills are replaced by the Universal small key. It is assured that at least one shop sells Universal Small Keys. The quiver may thus not be found in shops. The quiver and small keys retain their original base price, but may be discounted. +## Logic Level + +### Overworld Glitches + +Set `--logic` to `owglitches` to make overworld glitches required in the logic. + +## Shuffle Links House + +In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now control whether Links House is shuffled or remains vanilla. Previously, inverted seeds had this behavior and would shuffle links house, but now if will only do so if this is specified. Now, also works for open modes, but links house is never shuffled in standard mode. + +## Reduce Flashing + +Accessibility option to reducing some flashing animations in the game. + +## Pseudo-boots + +Option to start with ability to dash, but not able to make any boots required logical checks or traversal. + +## Experimental Features + +The treasure check counter is turned on. Also, you will start as a bunny if your spawn point is in the dark world. + +## Triforce Hunt Settings + +A collection of settings to control the triforce piece pool. + +* --triforce_goal_min: Minimum number of pieces to collect to win +* --triforce_goal_max: Maximum number of pieces to collect to win +* --triforce_pool_min: Minimum number of pieces in item pool +* --triforce_pool_max: Maximum number of pieces in item pool +* --triforce_min_difference: Minimum difference between pool and goal to win ## Seed @@ -280,6 +313,24 @@ Include mobs and pots drop in the item pool. (default: not enabled) Includes shop locations in the item pool. +``` +--pseudoboots +``` + +Start with dash ability, but no way to use boots to accomplish checks + +``` +--shufflelinks +``` + +Whether to shuffle links house in most ER modes. + +``` +--experimental +``` + +Enables experimental features + ``` --mixed_travel ``` @@ -290,4 +341,10 @@ How to handle certain glitches in crossed dungeon mode. (default: prevent) --standardize_palettes (mode) ``` -Whether to standardize dungeon palettes in crossed dungeon mode. (default: standardize) \ No newline at end of file +Whether to standardize dungeon palettes in crossed dungeon mode. (default: standardize) + +``` +--reduce_flashing +``` + +Reduces amount of flashing in some animations \ No newline at end of file From a1eb077f285732583b6680213a1631a8ac7dce0d Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 08:58:31 -0600 Subject: [PATCH 002/293] Boss music fix --- RELEASENOTES.md | 138 +----------------------------------------- Rom.py | 2 +- data/base2current.bps | Bin 136267 -> 136271 bytes 3 files changed, 3 insertions(+), 137 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0ccceb80..163c6087 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,138 +1,4 @@ -# New Features +### Bug Fix -## Maiden Hint for Theives Town Attic +Fix for boss music in non-DR modes (Thanks codemann8) -In crossed dungeon mode, if you bring the maiden to the boss room when the attic is not bombed (and thus no light in the room), she mentions the dungeon where you can find the cracked floor. - -## Shuffle Links House - -Links house can now be shuffled in different ER settings. It will be limited to the Light World (or Dark World in inverted) if Crossed or Insanity shuffle is not one. It it also limited if door shuffle settings allow the Sanctuary to be in the dark world. (This is prevent having no Light World spawn points in Open modes) This setting is ignored by standard mode. THe CLI parameter is --shufflelinks - -## OWG Glitch Logic - -Thanks to qadan, cheuer, & compiling - -## Pseudo Boots - -Thanks to Bonta. You can now start with pseudo boots that let you move fast, but have no other logical uses (bonking open things, hovering, etc) - -## Pendant/Crystal Indicator - -For accessibility, you now get a C or P indicator to the left of the magic bar on the HUD when instead a Crystal or Pendant. Requires ownership of the map of that dungeon for display. Thanks to kan. - -# Bug Fixes and Notes. - -* 0.4.0.12 - * ER Inverted fix for HC Ledge, and Aga Tower choosing Links House incorrectly - * Credits again - hopefully for good - * Incorporated music fixes for now (may revisit later) - * Secure random re-incorporated -* 0.4.0.11 - * Some minor base rom fixes - * Improved distribution of bombable/dashable doors -* 0.4.0.10 - * Renamed to pseudoboots - * Some release note updates -* 0.4.0.9 - * Fixes for stats and P/C indicator (thanks Kara) - * Swamp lobby fixes (thanks Catobat) - * Fix for --hints flag on CLI -* 0.4.0.8 - * Ganon jokes added for when silvers aren't available - * Some text updated (Blind jokes, uncle text) - * Fixed some enemizer Mystery settings - * Added a setting that's random enemy shuffle without Unkillable Thieves possible - * Fixed shop spoiler when money balancing/multiworld balancing - * Fixed a problem with insanity - * Fixed an issue with the credit stats specific to DR (e.g. collection rate total) - * More helpful error message when bps is missing? - * Minor generation issues involving enemizer and the link sprite - * Baserom updates (from Bonta, kan, qwertymodo, ardnaxelark) - * Boss icon on dungeon map (if you have a compass) - * Progressive bow sprite replacement - * Quickswap - consecutive special swaps - * Bonk Counter - * One mind - * MSU fix - * Chest turn tracking (not yet in credits) - * Damaged and magic stats in credits (gt bk removed) - * Fix for infinite bombs - * Pseudo boots option - * Always allowed medallions for swordless (no option yet) -* 0.4.0.7 - * Reduce flashing option added - * Sprite author credit added - * Ranged Crystal switch rules tweaked - * Baserom update: includes Credits Speedup, reduced flashing option, msu resume (but turned off by default) - * Create link sprite's zspr from local ROM and no longer attempts to download it from website - * Some minor bug fixes -* 0.4.0.6 - * Hints now default to off - * The maiden gives you a hint to the attic if you bring her to the unlit boss room - * Beemizer support and fix for shopsanity - * Capacity upgrades removed in hard/expert item difficulties - * Swamp Hub added to lobby shuffle with ugly cave entrance. - * TR Lava Escape added to lobby shuffle. - * Hyrule Main Lobby and Sanctuary can now have a more visible outside exit, and rugs modified to be fully clipped. -* 0.4.0.5 - * Insanity - less restrictions on exiting (all modes) - * Fix for simple bosses shuffle - * Fix for boss shuffle from Mystery.py - * Minor msu fade out bug (thanks codemann8) - * Other bug fixes (thanks Catobat) -* 0.4.0.4 - * Added --shufflelinks option - * Moved spawning as a bunny indoors to experimental - * Baserom bug fixes -* 0.4.0.3 - * Fixed a bug where Sanctuary could be chosen as a lobby for a DW dungeon in non-crossed ER modes -* 0.4.0.2 - * Fixed a bug where Defeat Ganon is not possible - * Fixed the item counter total - * Fixed the bunny state when starting out in Sanc in a dark world dungeon -* 0.4.0.1 - * Moved stonewall pre-opening to not happen in experimental - * Updated baserom - * Boss RNG perseved between files - * Vanilla prize pack fix - * Starting equipment fix - * Post-Aga world state option - * Code optimzation - * Bottle quickswap via double shoulder - * Credits update - * Accessibility option - * Sewer map/compass fix - * Fixed a standard bug where the exits to the ledge would be unavailable if the pyramid was pre-opened - * DR ASM optimization - * Removed Archery Game from Take-Any caves in inverted - * Fixed a problem with new YAML parser -* 0.4.0.0 - * Mystery yaml parser updated to a package maintained version (Thanks StructuralMike) - * Bomb-logic and extend crystal switch logic (Thanks StructuralMike) - * Fixed logic for moved locations in playthrough (Thanks compiling) - * OWG Glitch logic added - -# Known Issues - -* Shopsanity Issues - * Hints for items in shops can be misleading (ER) - * Forfeit in Multiworld not granting all shop items -* Potential keylocks in multi-entrance dungeons -* Incorrect vanilla key logic for Mire - -## Other Notes - -### Triforce Hunt Options - -Thanks to deathFouton! - ---triforce_pool and --triforce_goal added to the CLI. - -Also, to the Mystery.py he added the following options: -* triforce_goal_min -* triforce_goal_max -* triforce_pool_min -* triforce_pool_max -* triforce_min_difference - -See the example yaml file for demonstrated usage. \ No newline at end of file diff --git a/Rom.py b/Rom.py index 40249d56..355509c0 100644 --- a/Rom.py +++ b/Rom.py @@ -30,7 +30,7 @@ from EntranceShuffle import door_addresses, exit_ids JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9c2878d1035bb3889784906a55a92a26' +RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index a904b07d50f9fadac099414d033b0c27b309563b..5f41171067dccc266f98a3c0b2daee5a07a03f06 100644 GIT binary patch delta 5253 zcmW+(30xD$8qef_a3_F(pb}OQ5Y&KLDIVZ~C?bL<#i*!Y@x`lXttafF0TZ$bVF+7X zFbfP|I9zC|h!+Z42%^SU>)Bex*0!iM6?>?C`WBPl%=h1KuJ4@V$}#cvW8z{g>WBgH zHp_n{I({Y4bevQq;#FUkE4KX?4K#;~aUwjU(4a8kRRh_DDjXyoBo!ToWfKCvK93~% zs@k(c9f=iv#|<=KNAzewe45BHRHnL@Y0$tx+HpcAFEA>?4F85=solsACozIVh{P(6 zGiCE5p2}Oza=wd*2YBaPX1{{-KsbnoIUpkBW;YV!Mr>*cH%n+TZUAB*;OJLkQ2ehN z4csBIU+$t%jrtn|f)xK{McfGPcZtNN>yRtAV0|ew=8zG)0ax>5 zMxC>vOGXcSuFYX!E}@gZu^9~<$RO0%YZxhv03+ZUVJwJ*M}^6bVGw<9z`lor!g%nH_(;0cEn;1+asT+Fg3rK|xE_U{H zdUF#EWN`XIxXpQ3N-G_hZA>G|6|c2JvU?Yj7)GLqLxeK4(ug&E#|#WabQ`ci&e<*0 zR-r*lV%j3KKZqc)$9hyjV*l#VGGxFO6U;!5K`|HJbWQ~ZIMyY?Z3+3o8pS&g85R9- zt&3u8;tjN4O=5k`OcG~^^q!%(7K`ZwVexi?2yZFJ>?H`B4IjDqgvcOr<<@XHkGwSx z@|J5<f#);Q{hs0b!%!NCoAs#|Kq89FHr?$JEKBdB>&@83EQ#i|0A?DI3<5(4T z&Qj|+CKUL?-k5~RJsPxMtA1C;$TUVQcqqU7)O}BwqUlLvq!sT{)sl1@o>SzzZ8X=qI3mPrmSuQRh*z)c6IuX!W_U_67 zV2S1V7xzV=7+Mc(08N(hxpM%hfS=`k4pv)!$r}uGe8Qkk3C4$yOx~jR48oL_H>P%B z@&Q80^6`*{TIM0oCb4?*Y{F`9+|kPC+4$8GC3ekSurju(dwXXOb1w2G~PP zJ6*}gn#!$&T$S`ju>?wwt#_W9SiS|nWs_h!c;Hxo?-A`A<#BC`_ABij?L+O)+TXQy z?O)o{kUbV8k4h>pw_))l%3$dknEsCp{KC*z@#!Q)+J)FB*Tw7_TyEvGS9D#Yc%w_q zao`g;%iQbQq^o_Ymf76!DA^t8+mI!`p-1+?3~C8H)UXER!N(1U{QQIef>UH#I6rn5u@{LepdZw{&E+a0Knj@YADOI(XUQ;sff04M&UA7&bYbnYLsAP45?O|~| zdmtOff%iPSOZ5ht`CcMpjd*DF4%1CyWPLrc%Je1SM3)+SjI36$oeCC{?KYR{BAR*l zp<^Vgr6L?jwM+FulMhaegv|u5Zz1+^0&t^F^cbrl&HN-`XA8E=UQ@3LiT)0$tYDty z(TAz?^kSS4!@t!P%&&APK_R-95a_4ihNs*RDfJZsZNbZH zxDP2C4|SE*jWm-XW#>`5@p22zWJ=itN>_e{X0oO1d=7Qd%w{ROfJ0X}#iVQ^hrZ<` zm$D0o;C-6;Rmv{n&>+n`ld{Piddtn|$)-?q@Nz)~lj+H(Qe?SH1(WT`F5!?@1+&?c zT?*IW?aWIFtKhT}!s4$+(eI3r^A>{$Pb*eLiuJvxYB6i-xRH z^P7rIH&q#61+!bK*T7TMEz@m+Hr-J>RoBo46}!rGj%?(nt4D;zbe7|u8R9BURb&@8 z1b%i1IvgUdm?q%xHu3F;VfX30W-~iqIOIj_bQ5Dr@W?lAl8hq%@iavqIx&Racvg?D zVE&b`%c%(0d}5i&^$@wVg4qwFxM?UwJ~d54$7w371Gr97b_Eq!SRT$bd$KDj4}#cf zlH-PH70d`P$|;|iM9Yg{Z54kgV&+sZvicI}j5r1uVl0FwEwW(&!$_0=Fw!v2%b$#( zG;mX@))Wn&S}ub*u=VS|_&n|mJCQ2+)F&Tq^vPGPkkAV`7zLeA{sjUdX!G-`y@2+< z2_1^@Wb@XMq|S?)2_xF3fqt0X_5_TCi%-o1Phi=paF7N&Pt5?^OWH@dk6)m#zHDOWJ+dYLac$f}OR%&3Z zQd6Em`UH`_K~&-}9bq-i;##~~RTU2PGvSys>%nkXcxGdqXI-bfI!<^r>XVMA#!7nzHNrcx>-txBDgQuhsApSiNCrU}*(XDvTD;tSyTv+Jfu z@6lF@*us~&O0$ewyGPq1W%UH|Ej=~{YJgh^_QCW*y zM-l8jYX{lz%(N=6;h=%ruJNu@HrDs^bSGlskVQXJG?JuGG?N_F%YsgBraavRe4b@j=fQAiM-+_hDd@av&FL&xL)K(Ee<}5Nl$ec&V!kFu0SB{C*mp5it^LQF1 zHEG0y_V^C8x&!U+KnE@$cmcIJ)~UIh2(?>vLMf}bnAwwQ#khdBxxg)iCSTnvdEGF+ z$?V!$`&XF6E|8^lQG23WZ^*J>+x5|L4x@gl6xzLNt$v%Fx@riqzL_4q^=Un}^=V1S zZo?L0QJuBplM6@^ZnfKlPo#Lf^$(j!meqw%w!Y**8uYhK6*_mxI^c5KJny9!dnK=q z&f9tVbKI7a*V`-khwH)G?mCQ))n7DEhPoRgJy&)jzfFfSp6dBhUagfPo49TJNm)DoD4C`xO2q|rypAad)d?ycBpjUj0g8v<#~+~k5S;b_N+f4`<$U~fznq$o zN|)F6_=kC@dQ7`5UH@(5gQalWce2Sokvd{L<>*vgJLgrd&50fVDmR9j;8gsmBd`nL z3D^uZkIg6KVSKx+*&SH-*@n};u zqLs6CR21yI6*>H~OdSzJ5BB%M-RHgRF|-K13b@b*WZYg`SrV_)U^ZJWwnpFCgz%z~kJ z3PBERxf97fl>6_LMvwf|5&uqc)h^<4-|SeK;vsXsiv99xlq~V8Y5Jt+rdf-AB*t}Y zcQ{(IGh2H!)9GlLcLDKFO$I!E*Jo7LM19GE;dE6Q1F-#gb+NlCeZ1aSEW}M%LcC=I~1wBr}5h)<25imnEZxf~1L1eScO!?y_HMMcL-*_{Z7! zNAsz0rrh=_+dKmgrRTz*@0U$Eeck-FQ&Pd3ZSo3`;;j{NLCtYNwzp=2*@ecvw+baT*~d3JZUp0l2iXD^G4t=8|6;Rfyz zU>i%tz_=f700MeG*aFr-{J{a>0N*`W9+-Rc*(6{eH1nPgNp5$8A%;lYG5SSAN*+Ea zbAUNN#{0&GIpJm=4m5h+vOJv&pk|J6!r?x6?Z+VQc5DA}IQKenet0%ab_>}ZdL@5r z@E$Wx1_gD>sddSY`0EB|=S{pa(sNEaBjfeUIwIhUhc%I&08%d+4bh>8rbz{0=Hkjd~^f&xaR4NFPITL z_FFjYv@yo|p4SpfErNF+2Y?9p=i`at6H|+cP%3RX4l{Uq;*;)tBAY9lEcqoKo3&81 z#9%m(-gJi#@72iQ;-3z9WX=4S$o^t?fq(iRi^J6`Ecbu%cXpbZcv$U$9i)};-S3-$ z3sk;HBYjNhFEtavrdSurmi6)2JW!!6&bl0VSOFXQc1!9r#7moqJAhm+hkTb=|p*8Smax z!z>5?d<`a4b)$RK$n&i(i47esTn3rA>diJhewc!tBzL?l#pKJl_{{gz^ai0JH delta 5364 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 q>=Zx2ly7Ye>5S6|F From bbad1d1d8b1020b50453b66b2d88c5fb8712be38 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 11:33:10 -0600 Subject: [PATCH 003/293] Version bump Mystery can apply reduce_flashing --- Main.py | 2 +- Mystery.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index e92bbf61..96235926 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0-dev' +__version__ = '0.4.1-dev' class EnemizerError(RuntimeError): diff --git a/Mystery.py b/Mystery.py index d3e3bddf..1ab4a90b 100644 --- a/Mystery.py +++ b/Mystery.py @@ -226,6 +226,7 @@ def roll_settings(weights): ret.sprite = get_choice('sprite', romweights) ret.disablemusic = get_choice('disablemusic', romweights) == 'on' ret.quickswap = get_choice('quickswap', romweights) == 'on' + ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on' ret.fastmenu = get_choice('menuspeed', romweights) ret.heartcolor = get_choice('heartcolor', romweights) ret.heartbeep = get_choice('heartbeep', romweights) From f259e8bdc8f3e248e9a912501a640ffd3024a7c0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Aug 2021 14:32:54 -0600 Subject: [PATCH 004/293] Boss item restriction logic added Reserved location logic started --- BaseClasses.py | 39 ++++++++++--- CLI.py | 3 +- DungeonGenerator.py | 56 +++++++++++++------ Dungeons.py | 8 +-- Fill.py | 15 +++-- KeyDoorShuffle.py | 55 ++++++++---------- Main.py | 4 ++ Mystery.py | 1 + Rules.py | 18 ++++++ resources/app/cli/args.json | 7 +++ resources/app/cli/lang/en.json | 7 +++ resources/app/gui/lang/en.json | 4 ++ resources/app/gui/randomize/item/widgets.json | 9 +++ source/classes/constants.py | 3 +- source/item/FillUtil.py | 20 +++++++ 15 files changed, 181 insertions(+), 68 deletions(-) create mode 100644 source/item/FillUtil.py diff --git a/BaseClasses.py b/BaseClasses.py index b9b3dd1c..3a2dc49b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -114,6 +114,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) + set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') @@ -324,7 +325,7 @@ class World(object): elif item.name.startswith('Bottle'): if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit: ret.prog_items[item.name, item.player] += 1 - elif item.advancement or item.smallkey or item.bigkey: + elif item.advancement or item.smallkey or item.bigkey or item.compass or item.map: ret.prog_items[item.name, item.player] += 1 for item in self.itempool: @@ -339,6 +340,8 @@ class World(object): key_list += [dungeon.big_key.name] if len(dungeon.small_keys) > 0: key_list += [x.name for x in dungeon.small_keys] + # map/compass may be required now + key_list += [x.name for x in dungeon.dungeon_items] from Items import ItemFactory for item in ItemFactory(key_list, p): soft_collect(item) @@ -2178,6 +2181,12 @@ class Item(object): item_dungeon = 'Hyrule Castle' return item_dungeon + def is_inside_dungeon_item(self, world): + return ((self.smallkey and not world.keyshuffle[self.player]) + 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 __str__(self): return str(self.__unicode__()) @@ -2388,6 +2397,7 @@ class Spoiler(object): 'weapons': self.world.swords, 'goal': self.world.goal, 'shuffle': self.world.shuffle, + 'linkshuffle': self.world.shufflelinks, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'item_pool': self.world.difficulty, @@ -2396,6 +2406,7 @@ class Spoiler(object): 'ganon_crystals': self.world.crystals_needed_for_ganon, 'open_pyramid': self.world.open_pyramid, 'accessibility': self.world.accessibility, + 'restricted_boss_items': self.world.restrict_boss_items, 'hints': self.world.hints, 'mapshuffle': self.world.mapshuffle, 'compassshuffle': self.world.compassshuffle, @@ -2411,6 +2422,7 @@ class Spoiler(object): 'experimental': self.world.experimental, 'keydropshuffle': self.world.keydropshuffle, 'shopsanity': self.world.shopsanity, + 'psuedoboots': self.world.pseudoboots, 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} @@ -2438,6 +2450,9 @@ class Spoiler(object): return json.dumps(out) def to_file(self, filename): + def yn(flag): + return 'Yes' if flag else 'No' + self.parse_data() with open(filename, 'w') as outfile: outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) @@ -2462,6 +2477,7 @@ class Spoiler(object): outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) + outfile.write(f"Links House Shuffled: {self.metadata['linkshuffle'][player]}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' @@ -2470,6 +2486,7 @@ class Spoiler(object): outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) @@ -2478,12 +2495,13 @@ class Spoiler(object): outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) - outfile.write('Pot shuffle: %s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) - outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) - outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) - outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) - outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") - outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) + outfile.write(f"Pot shuffle: {yn(self.metadata['potshuffle'][player])}\n") + outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Key Drops shuffled: {yn(self.metadata['keydropshuffle'][player])}\n") + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") + outfile.write(f"Pseudoboots: {yn(self.metadata['bombbag'][player])}\n") if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2665,6 +2683,12 @@ enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3} e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} +# additions +# shuffle links: 1 bit +# restrict_boss_mode: 2 bits +# psuedoboots does not effect code +# sfx_shuffle and other adjust items does not effect settings code + class Settings(object): @staticmethod @@ -2738,7 +2762,6 @@ class Settings(object): args.shufflepots[p] = True if settings[7] & 0x4 else False -@unique class KeyRuleType(FastEnum): WorstCase = 0 AllowSmall = 1 diff --git a/CLI.py b/CLI.py index bb9ab0a2..bb0af91b 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -140,6 +140,7 @@ def parse_settings(): "progressive": "on", "accessibility": "items", "algorithm": "balanced", + "restrict_boss_items": "none", # Shuffle Ganon defaults to TRUE "openpyramid": False, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 948115f3..4be27692 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -226,7 +226,8 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) - dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception) + dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception, + world, player) either_crystal = True # if all hooks from the origin are either, explore all bits with either for hook, crystal in dungeon['Origin'].hooks.items(): if crystal != CrystalBarrier.Either: @@ -247,7 +248,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) o_state_cache[door.name] = o_state - piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception) + piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception, world, player) dungeon[door.name] = piece check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors, group_flags, door_map, world, player, exception) @@ -347,7 +348,7 @@ def explore_blue_state(door, dungeon, o_state, proposed_map, all_regions, valid_ blue_start.big_key_special = o_state.big_key_special b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) - dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception) + dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player) def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name): @@ -639,7 +640,7 @@ def stonewall_valid(stonewall): return True -def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception): +def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player): # todo: info about dungeon events - not sure about that graph_piece = GraphPiece() all_unattached = {} @@ -671,16 +672,14 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio graph_piece.visited_regions.update(o_state.visited_orange) graph_piece.visited_regions.update(b_state.visited_blue) graph_piece.visited_regions.update(b_state.visited_orange) - graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found)) - graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found, world, player)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found, world, player)) graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used return graph_piece -def filter_for_potential_bk_locations(locations): - return [x for x in locations if - '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events - and not x.forced_item and x.name not in ['Agahnim 1', 'Agahnim 2']] +def filter_for_potential_bk_locations(locations, world, player): + return count_locations_exclude_big_chest(locations, world, player) type_map = { @@ -1023,12 +1022,8 @@ class ExplorationState(object): return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal return True - def count_locations_exclude_specials(self): - cnt = 0 - for loc in self.found_locations: - if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and not loc.forced_item: - cnt += 1 - return cnt + def count_locations_exclude_specials(self, world, player): + return count_locations_exclude_big_chest(self.found_locations, world, player) def validate(self, door, region, world, player): return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, @@ -1069,6 +1064,32 @@ class ExplorationState(object): return 2 +def count_locations_exclude_big_chest(locations, world, player): + cnt = 0 + for loc in locations: + if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)): + cnt += 1 + return cnt + + +def prize_or_event(loc): + return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + + +def reserved_location(loc, world, player): + return loc.name in world.item_pool_config.reserved_locations[player] + + +def blind_boss_unavail(loc, locations, world, player): + if loc.name == "Thieves' Town - Boss": + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in locations if x.name == 'Attic Cracked Floor')))) + return False + + class ExplorableDoor(object): def __init__(self, door, crystal, flag): @@ -1092,7 +1113,8 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg explorable_door = local_state.next_avail_door() if explorable_door.door.bigKey: if bk_flag: - big_not_found = not special_big_key_found(local_state) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0 + big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special + else local_state.count_locations_exclude_specials(world, player) == 0) if big_not_found: continue # we can't open this door if explorable_door.door in proposed_map: diff --git a/Dungeons.py b/Dungeons.py index 73f53794..a37ce3d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -117,6 +117,7 @@ def fill_dungeons(world): def get_dungeon_item_pool(world): return [item for dungeon in world.dungeons for item in dungeon.all_items] + def fill_dungeons_restrictive(world, shuffled_locations): all_state_base = world.get_all_state() @@ -137,10 +138,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): 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.smallkey and not world.keyshuffle[item.player]) - or (item.bigkey and not world.bigkeyshuffle[item.player]) - or (item.map and not world.mapshuffle[item.player]) - or (item.compass and not world.compassshuffle[item.player]))] + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] # sort in the order Big Key, Small Key, Other before placing dungeon items sort_order = {"BigKey": 3, "SmallKey": 2} @@ -414,7 +412,7 @@ dungeon_prize = { 'Palace of Darkness': 'Palace of Darkness - Prize', 'Swamp Palace': 'Swamp Palace - Prize', 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': 'Thieves Town - Prize', + 'Thieves Town': "Thieves' Town - Prize", 'Ice Palace': 'Ice Palace - Prize', 'Misery Mire': 'Misery Mire - Prize', 'Turtle Rock': 'Turtle Rock - Prize', diff --git a/Fill.py b/Fill.py index c42b6251..67476fbf 100644 --- a/Fill.py +++ b/Fill.py @@ -237,7 +237,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = def valid_key_placement(item, location, itempool, world): - if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player] or world.logic[item.player] == 'nologic': + if not valid_reserved_placement(item, location, world): + return False + if ((not item.smallkey and not item.bigkey) or item.player != location.player + or world.retro[item.player] or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon if dungeon: @@ -247,9 +250,13 @@ def valid_key_placement(item, location, itempool, world): unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) else: - inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) - or (item.bigkey and not world.bigkeyshuffle[item.player])) - return not inside_dungeon_item + return item.is_inside_dungeon_item(world) + + +def valid_reserved_placement(item, location, world): + if item.player == location.player and item.is_inside_dungeon_item(world): + return location.name not in world.item_pool_config.reserved_locations[location.player] + return True def track_outside_keys(item, location, world): diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index cf18f4f4..c5c666e4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,8 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize -from DungeonGenerator import ExplorationState, special_big_key_doors +from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import reserved_location, blind_boss_unavail class KeyLayout(object): @@ -1078,40 +1079,30 @@ def location_is_bk_locked(loc, key_logic): return loc in key_logic.bk_chests or loc in key_logic.bk_locked -def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] - - -def boss_unavail(loc, world, player): - # todo: ambrosia - # return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name - return False - - -def blind_boss_unavail(loc, state, world, player): - if loc.name == "Thieves' Town - Boss": - # todo: check attic - return (loc.parent_region.dungeon.boss.name == 'Blind' and - (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or - (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and - not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) - return False +# 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'] +# +# +# def reserved_location(loc, world, player): +# return loc in world.item_pool.config.reserved_locations[player] +# +# +# def blind_boss_unavail(loc, state, world, player): +# if loc.name == "Thieves' Town - Boss": +# return (loc.parent_region.dungeon.boss.name == 'Blind' and +# (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or +# (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and +# not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) +# return False +# counts free locations for keys - hence why reserved locations don't count def count_free_locations(state, world, player): cnt = 0 for loc in state.found_locations: - if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) - and not blind_boss_unavail(loc, state, world, player)): - cnt += 1 - return cnt - - -def count_locations_exclude_big_chest(state, world, player): - cnt = 0 - for loc in state.found_locations: - if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player) - and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)): + if (not prize_or_event(loc) and not loc.forced_item and not reserved_location(loc, world, player) + and not blind_boss_unavail(loc, state.found_locations, world, player)): cnt += 1 return cnt @@ -1407,7 +1398,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: - ttl_locations = count_locations_exclude_big_chest(state, world, player) + ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) @@ -1596,7 +1587,7 @@ def can_open_door(door, state, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: - ttl_locations = count_locations_exclude_big_chest(state, world, player) + ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) if door.smallKey: ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) diff --git a/Main.py b/Main.py index ee9e83fb..f6a94ce9 100644 --- a/Main.py +++ b/Main.py @@ -28,6 +28,8 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names +from source.item.FillUtil import create_item_pool_config + __version__ = '0.5.1.0-u' from source.classes.BabelFish import BabelFish @@ -103,6 +105,7 @@ def main(args, seed=None, fish=None): world.treasure_hunt_total = args.triforce_pool.copy() world.shufflelinks = args.shufflelinks.copy() world.pseudoboots = args.pseudoboots.copy() + world.restrict_boss_items = args.restrict_boss_items.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} @@ -146,6 +149,7 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) + create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) diff --git a/Mystery.py b/Mystery.py index 73644500..a53fb514 100644 --- a/Mystery.py +++ b/Mystery.py @@ -132,6 +132,7 @@ def roll_settings(weights): ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] ret.accessibility = get_choice('accessibility') + ret.restrict_boss_items = get_choice('restrict_boss_items') entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' diff --git a/Rules.py b/Rules.py index 28d627da..f06328c5 100644 --- a/Rules.py +++ b/Rules.py @@ -4,6 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType +from Dungeons import dungeon_regions, dungeon_prize from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -553,11 +554,28 @@ def global_rules(world, player): add_key_logic_rules(world, player) # End of door rando rules. + if world.restrict_boss_items[player] != 'none': + def add_mc_rule(l): + boss_location = world.get_location(l, player) + d_name = boss_location.parent_region.dungeon.name + compass_name = f'Compass ({d_name})' + map_name = f'Map ({d_name})' + add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) + + for dungeon in dungeon_prize.keys(): + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: + add_mc_rule(loc) + if world.doorShuffle[player] == 'crossed': + add_mc_rule('Agahnim 1') + add_mc_rule('Agahnim 2') + add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player) and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop + def bomb_rules(world, player): bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave', diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a0113222..da496104 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -254,6 +254,13 @@ "none" ] }, + "restrict_boss_items": { + "choices": [ + "none", + "mapcompass", + "dungeon" + ] + }, "hints": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 67efab1d..13c99b2a 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -276,6 +276,13 @@ "Locations: You will be able to reach every location in the game.", "None: You will be able to reach enough locations to beat the game." ], + "restrict_boss_items": [ + "Select which dungeon are not allowed on bosses (default: %(default)s)", + "None: All items allowed", + "Mapcompass: Map and Compass are required before you defeat the boss.", + "Dungeon: Same as above and keys too cannot be on the boss. Small key shuffle", + " and big key shuffle override this behavior" + ], "hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ], "shuffleganon": [ "Include the Ganon's Tower and Pyramid Hole in the", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c4cd8a11..ae26b0dd 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -287,6 +287,10 @@ "randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.balanced": "Balanced", + "randomizer.item.restrict_boss_items": "Forbidden Boss Items", + "randomizer.item.restrict_boss_items.none": "None", + "randomizer.item.restrict_boss_items.mapcompass": "Map & Compass", + "randomizer.item.restrict_boss_items.dungeon": "Map & Compass & Keys", "bottom.content.worlds": "Worlds", "bottom.content.names": "Player names", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 038e668c..89cacb00 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -124,6 +124,15 @@ "vt26", "balanced" ] + }, + "restrict_boss_items": { + "type": "selectbox", + "default": "none", + "options": [ + "none", + "mapcompass", + "dungeon" + ] } } } diff --git a/source/classes/constants.py b/source/classes/constants.py index b184643b..64b01520 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -72,7 +72,8 @@ SETTINGSTOPROCESS = { "progressives": "progressive", "accessibility": "accessibility", "sortingalgo": "algorithm", - "beemizer": "beemizer" + "beemizer": "beemizer", + "restrict_boss_items": "restrict_boss_items" }, "entrance": { "openpyramid": "openpyramid", diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py new file mode 100644 index 00000000..958c6b67 --- /dev/null +++ b/source/item/FillUtil.py @@ -0,0 +1,20 @@ +from collections import defaultdict + +from Dungeons import dungeon_prize + +class ItemPoolConfig(object): + + def __init__(self): + self.reserved_locations = defaultdict(set) + + +def create_item_pool_config(world): + config = ItemPoolConfig() + if world.algorithm in ['balanced']: + for player in range(1, world.players+1): + if world.restrict_boss_items[player]: + for dungeon in dungeon_prize: + if dungeon.startswith('Thieves'): + dungeon = "Thieves' Town" + config.reserved_locations[player].add(f'{dungeon} - Boss') + world.item_pool_config = config From 6f06dbcd04ee1579c88d136d9ed088f0c63afee7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 005/293] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 45 ++++++++++++++++++++++++++++++++------------- KeyDoorShuffle.py | 10 +++++----- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3a2dc49b..b35e53da 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -441,8 +441,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index a37ce3d4..2edba0d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -373,6 +373,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -405,19 +437,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': "Thieves' Town - Prize", - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} - dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5c666e4..5c65dbe6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1378,7 +1378,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1541,7 +1541,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1966,8 +1966,8 @@ def validate_key_placement(key_layout, world, 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) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + 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) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 4d776e0fee2c49d7cc208dffbd75fde5f6a2d48c Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:25:29 -0600 Subject: [PATCH 006/293] Compass/Map can be progressive Fixed filter_for_potential_bk_locations Changed rules to use dungeon_table --- BaseClasses.py | 8 ++++---- DungeonGenerator.py | 5 +++-- Rules.py | 11 ++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b35e53da..67f2f93d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -852,7 +852,7 @@ class CollectionState(object): reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item - if item_player == player and self.check_if_progressive(item_name): + if item_player == player and self.check_if_progressive(item_name, player): if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: @@ -868,8 +868,7 @@ class CollectionState(object): reduced[('Heart Container', player)] = 1 return frozenset(reduced.items()) - @staticmethod - def check_if_progressive(item_name): + def check_if_progressive(self, item_name, player): return (item_name in ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', @@ -881,7 +880,8 @@ class CollectionState(object): 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] - or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) + or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass')))) def can_reach(self, spot, resolution_hint=None, player=None): try: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 4be27692..a2c3df04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -679,7 +679,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return count_locations_exclude_big_chest(locations, world, player) + return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] type_map = { @@ -1078,7 +1079,7 @@ def prize_or_event(loc): def reserved_location(loc, world, player): - return loc.name in world.item_pool_config.reserved_locations[player] + return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player] def blind_boss_unavail(loc, locations, world, player): diff --git a/Rules.py b/Rules.py index f06328c5..dec41c79 100644 --- a/Rules.py +++ b/Rules.py @@ -4,7 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType -from Dungeons import dungeon_regions, dungeon_prize +from Dungeons import dungeon_table from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -562,10 +562,11 @@ def global_rules(world, player): map_name = f'Map ({d_name})' add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) - for dungeon in dungeon_prize.keys(): - d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon - for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: - add_mc_rule(loc) + for dungeon, info in dungeon_table.items(): + if info.prize: + 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': add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') From 746a73933980057af6c15de7a6b860eaed958874 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:27:05 -0600 Subject: [PATCH 007/293] Main structure for various biased fills Lots of help around correctly reserving locations --- BaseClasses.py | 13 + Bosses.py | 4 +- DoorShuffle.py | 33 +- DungeonGenerator.py | 42 +- Dungeons.py | 114 --- Fill.py | 403 ++++---- ItemList.py | 13 +- KeyDoorShuffle.py | 2 + Main.py | 64 +- Mystery.py | 11 +- Regions.py | 8 + resources/app/cli/args.json | 12 +- resources/app/cli/lang/en.json | 40 +- resources/app/gui/lang/en.json | 11 +- resources/app/gui/randomize/item/widgets.json | 13 +- source/item/BiasedFill.py | 881 ++++++++++++++++++ source/item/FillUtil.py | 20 - 17 files changed, 1239 insertions(+), 445 deletions(-) create mode 100644 source/item/BiasedFill.py delete mode 100644 source/item/FillUtil.py diff --git a/BaseClasses.py b/BaseClasses.py index 67f2f93d..702f5436 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -17,6 +17,7 @@ from Utils import int16_as_bytes from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from RoomData import Room + class World(object): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, @@ -213,6 +214,11 @@ class World(object): return r_location raise RuntimeError('No such location %s for player %d' % (location, player)) + def get_location_unsafe(self, location, player): + if (location, player) in self._location_cache: + return self._location_cache[(location, player)] + return None + def get_dungeon(self, dungeonname, player): if isinstance(dungeonname, Dungeon): return dungeonname @@ -1452,6 +1458,10 @@ class Dungeon(object): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' +class FillError(RuntimeError): + pass + + @unique class DoorType(Enum): Normal = 1 @@ -1842,6 +1852,8 @@ class Sector(object): self.destination_entrance = False self.equations = None self.item_logic = set() + self.chest_location_set = set() + def region_set(self): if self.r_name_set is None: @@ -2084,6 +2096,7 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.locked = False + self.real = not crystal self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.item_rule = lambda item: True diff --git a/Bosses.py b/Bosses.py index 2718431e..5c742015 100644 --- a/Bosses.py +++ b/Bosses.py @@ -1,8 +1,8 @@ import logging import RaceRandom as random -from BaseClasses import Boss -from Fill import FillError +from BaseClasses import Boss, FillError + def BossFactory(boss, player): if boss is None: diff --git a/DoorShuffle.py b/DoorShuffle.py index ca377c65..dc0f18f5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -46,8 +46,7 @@ def link_doors(world, player): world.swamp_patch_required[player] = orig_swamp_patch -def link_doors_main(world, player): - +def link_doors_prep(world, player): # Drop-down connections & push blocks for exitName, regionName in logical_connections: connect_simple_door(world, exitName, regionName, player) @@ -99,6 +98,7 @@ def link_doors_main(world, player): analyze_portals(world, player) for portal in world.dungeon_portals[player]: connect_portal(portal, world, player) + if not world.doorShuffle[player] == 'vanilla': fix_big_key_doors_with_ugly_smalls(world, player) else: @@ -119,11 +119,14 @@ def link_doors_main(world, player): for ent, ext in default_one_way_connections: connect_one_way(world, ent, ext, player) vanilla_key_logic(world, player) - elif world.doorShuffle[player] == 'basic': + + +def link_doors_main(world, player): + if world.doorShuffle[player] == 'basic': within_dungeon(world, player) elif world.doorShuffle[player] == 'crossed': cross_dungeon(world, player) - else: + elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) @@ -214,11 +217,33 @@ def vanilla_key_logic(world, player): world.key_logic[player] = {} analyze_dungeon(key_layout, world, player) world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout log_key_logic(builder.name, key_layout.key_logic) # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) +def validate_vanilla_reservation(dungeon, world, player): + return validate_key_layout(world.key_layout[player][dungeon.name], world, player) + # if not hasattr(world, 'builder_cache'): + # world.builder_cache = {} + # if (dungeon.name, player) not in world.builder_cache: + # sector = Sector() + # sector.name = dungeon.name + # sector.regions.extend(convert_regions(dungeon.regions, world, player)) + # builder = simple_dungeon_builder(sector.name, [sector]) + # builder.master_sector = sector + # + # origin_list = find_accessible_entrances(world, player, builder) + # start_regions = convert_regions(origin_list, world, player) + # doors = convert_key_doors(default_small_key_doors[builder.name], world, player) + # key_layout = build_key_layout(builder, start_regions, doors, world, player) + # world.builder_cache[(dungeon.name, player)] = key_layout + # else: + # key_layout = world.builder_cache[(dungeon.name, player)] + # return validate_key_layout(key_layout, world, player) + + # some useful functions oppositemap = { Direction.South: Direction.North, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index a2c3df04..1b4b3564 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1198,6 +1198,8 @@ class DungeonBuilder(object): self.sectors = [] self.location_cnt = 0 self.key_drop_cnt = 0 + self.dungeon_items = None # during fill how many dungeon items are left + self.free_items = None # during fill how many dungeon items are left self.bk_required = False self.bk_provided = False self.c_switch_required = False @@ -1359,7 +1361,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, polarized_sectors[sector] = None if bow_sectors: assign_bow_sectors(dungeon_map, bow_sectors, global_pole) - assign_location_sectors(dungeon_map, free_location_sectors, global_pole) + assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) for sector in leftover: @@ -1510,6 +1512,7 @@ def define_sector_features(sectors): sector.bk_provided = True elif loc.name not in dungeon_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", "Thieves' Town - Blind's Cell"]: sector.bk_required = True @@ -1590,19 +1593,26 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): assign_sector(sector_list[i], builder, bow_sectors, global_pole) -def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): +def assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player): valid = False choices = None sector_list = list(free_location_sectors) random.shuffle(sector_list) + orig_location_set = build_orig_location_set(dungeon_map) + num_dungeon_items = requested_dungeon_items(world, player) while not valid: choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) + location_set = {x: set(y) for x, y in orig_location_set.items()} for i, sector in enumerate(sector_list): - choice = d_idx[choices[i].name] + d_name = choices[i].name + choice = d_idx[d_name] totals[choice] += sector.chest_locations + location_set[d_name].update(sector.chest_location_set) valid = True for d_name, idx in d_idx.items(): - if totals[idx] < 5: # min locations for dungeons is 5 (bk exception) + free_items = count_reserved_locations(world, player, location_set[d_name]) + target = max(free_items, 2) + num_dungeon_items + if totals[idx] < target: valid = False break for i, choice in enumerate(choices): @@ -1633,6 +1643,30 @@ def weighted_random_locations(dungeon_map, free_location_sectors): return choices, d_idx, totals +def build_orig_location_set(dungeon_map): + orig_locations = {} + for name, builder in dungeon_map.items(): + orig_locations[name] = set().union(*(s.chest_location_set for s in builder.sectors)) + return orig_locations + + +def requested_dungeon_items(world, player): + num = 0 + if not world.bigkeyshuffle[player]: + num += 1 + if not world.compassshuffle[player]: + num += 1 + if not world.mapshuffle[player]: + num += 1 + return num + + +def count_reserved_locations(world, player, proposed_set): + if world.item_pool_config: + return len([x for x in proposed_set if x in world.item_pool_config.reserved_locations[player]]) + return 2 + + def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): population = [] some_c_switches_present = False diff --git a/Dungeons.py b/Dungeons.py index 2edba0d4..ce57e7f8 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -1,8 +1,5 @@ -import RaceRandom as random - from BaseClasses import Dungeon from Bosses import BossFactory -from Fill import fill_restrictive from Items import ItemFactory @@ -36,117 +33,6 @@ def create_dungeons(world, player): world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] -def fill_dungeons(world): - freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest'] - - all_state_base = world.get_all_state() - - for player in range(1, world.players + 1): - pinball_room = world.get_location('Skull Woods - Pinball Room', player) - if world.retro[player]: - world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - else: - world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - pinball_room.event = True - pinball_room.locked = True - - dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons] - - loopcnt = 0 - while dungeons: - loopcnt += 1 - dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0) - # this is what we need to fill - dungeon_locations = [location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions] - random.shuffle(dungeon_locations) - - all_state = all_state_base.copy() - - # first place big key - if big_key is not None: - bk_location = None - for location in dungeon_locations: - if location.item_rule(big_key): - bk_location = location - break - - if bk_location is None: - raise RuntimeError('No suitable location for %s' % big_key) - - world.push_item(bk_location, big_key, False) - bk_location.event = True - bk_location.locked = True - dungeon_locations.remove(bk_location) - big_key = None - - # next place small keys - while small_keys: - small_key = small_keys.pop() - all_state.sweep_for_events() - sk_location = None - for location in dungeon_locations: - if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)): - sk_location = location - break - - if sk_location is None: - # need to retry this later - small_keys.append(small_key) - dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items)) - # infinite regression protection - if loopcnt < (30 * world.players): - break - else: - raise RuntimeError('No suitable location for %s' % small_key) - - world.push_item(sk_location, small_key, False) - sk_location.event = True - sk_location.locked = True - dungeon_locations.remove(sk_location) - - if small_keys: - # key placement not finished, loop again - continue - - # next place dungeon items - for dungeon_item in dungeon_items: - di_location = dungeon_locations.pop() - world.push_item(di_location, dungeon_item, False) - - -def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items] - - -def fill_dungeons_restrictive(world, shuffled_locations): - all_state_base = world.get_all_state() - - # for player in range(1, world.players + 1): - # pinball_room = world.get_location('Skull Woods - Pinball Room', player) - # if world.retro[player]: - # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - # else: - # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - # pinball_room.event = True - # pinball_room.locked = True - # shuffled_locations.remove(pinball_room) - - # 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]) 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)] - - # sort in the order Big Key, Small Key, Other before placing dungeon items - sort_order = {"BigKey": 3, "SmallKey": 2} - dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - - fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, - keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, single_player_placement=True) - dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], 'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E], diff --git a/Fill.py b/Fill.py index 67476fbf..13de2a20 100644 --- a/Fill.py +++ b/Fill.py @@ -3,176 +3,74 @@ import collections import itertools import logging -from BaseClasses import CollectionState +from BaseClasses import CollectionState, FillError from Items import ItemFactory from Regions import shop_to_location_table, retro_shops +from source.item.BiasedFill import filter_locations, classify_major_items, split_pool -class FillError(RuntimeError): - pass - -def distribute_items_cutoff(world, cutoffrate=0.33): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) - - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - - total_advancement_items = len([item for item in itempool if item.advancement]) - placed_advancement_items = 0 - - progress_done = False - advancement_placed = False - - # sweep once to pick up preplaced items - world.state.sweep_for_events() - - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - placed_advancement_items += 1 - break - - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - placed_advancement_items += 1 - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise FillError('No more progress items left to place.') - - spot_to_fill = None - for location in fill_locations if placed_advancement_items / total_advancement_items < cutoffrate else reversed(fill_locations): - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break - - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise FillError('No more spots to place %s' % item_to_place) - - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) - unplaced = [item.name for item in itempool] - unfilled = [location.name for location in fill_locations] - if unplaced or unfilled: - logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) +def get_dungeon_item_pool(world): + return [item for dungeon in world.dungeons for item in dungeon.all_items] -def distribute_items_staleness(world): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) +def promote_dungeon_items(world): + world.itempool += get_dungeon_item_pool(world) - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool + for item in world.get_items(): + if item.smallkey or item.bigkey: + item.advancement = True + elif item.map or item.compass: + item.priority = True + dungeon_tracking(world) - progress_done = False - advancement_placed = False - # sweep once to pick up preplaced items - world.state.sweep_for_events() +def dungeon_tracking(world): + for dungeon in world.dungeons: + layout = world.dungeon_layouts[dungeon.player][dungeon.name] + layout.dungeon_items = len(dungeon.all_items) + layout.free_items = layout.location_cnt - layout.dungeon_items - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise FillError('No more progress items left to place.') +def fill_dungeons_restrictive(world, shuffled_locations): + dungeon_tracking(world) + all_state_base = world.get_all_state() - spot_to_fill = None - for location in fill_locations: - # increase likelyhood of skipping a location if it has been found stale - if not progress_done and random.randint(0, location.staleness_count) > 2: - continue + # for player in range(1, world.players + 1): + # pinball_room = world.get_location('Skull Woods - Pinball Room', player) + # if world.retro[player]: + # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) + # else: + # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) + # pinball_room.event = True + # pinball_room.locked = True + # shuffled_locations.remove(pinball_room) - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break - else: - location.staleness_count += 1 + # 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]) 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 - # might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate - if spot_to_fill is None: - for location in fill_locations: - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise FillError('No more spots to place %s' % item_to_place) + # sort in the order Big Key, Small Key, Other before placing dungeon items + sort_order = {"BigKey": 3, "SmallKey": 2} + dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) + fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, + keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, + single_player_placement=True) - unplaced = [item.name for item in itempool] - unfilled = [location.name for location in fill_locations] - if unplaced or unfilled: - logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) -def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = None, single_player_placement = False): +def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, + reserved_items=None): + if not reserved_items: + reserved_items = [] + def sweep_from_pool(): new_state = base_state.copy() - for item in itempool: + for item in itempool + reserved_items: new_state.collect(item, True) new_state.sweep_for_events() return new_state @@ -201,41 +99,56 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None - for location in locations: - if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there - location.item = item_to_place - test_state = maximum_exploration_state.copy() - test_state.stale[item_to_place.player] = True - else: - test_state = maximum_exploration_state - if (not single_player_placement or location.player == item_to_place.player)\ - and location.can_fill(test_state, item_to_place, perform_access_check)\ - and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location + item_locations = filter_locations(item_to_place, locations, world) + for location in item_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, + single_player_placement, perform_access_check, itempool, + keys_in_itempool, world) + if spot_to_fill: break - if item_to_place.smallkey or item_to_place.bigkey: - location.item = None - if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) if world.can_beat_game(): if world.accessibility[item_to_place.player] != 'none': - logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {item_to_place})') continue - spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, - base_state, itempool, keys_in_itempool, single_player_placement) + if world.algorithm in ['balanced', 'equitable']: + spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, keys_in_itempool, + single_player_placement) if spot_to_fill is None: raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) track_outside_keys(item_to_place, spot_to_fill, world) + track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) spot_to_fill.event = True itempool.extend(unplaced_items) +def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, + itempool, keys_in_itempool, world): + if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + location.item = item_to_place + test_state = max_exp_state.copy() + test_state.stale[item_to_place.player] = True + else: + test_state = max_exp_state + if not single_player_placement or location.player == item_to_place.player: + if location.can_fill(test_state, item_to_place, perform_access_check): + test_pool = itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool + if valid_key_placement(item_to_place, location, test_pool, world): + if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): + return location + if item_to_place.smallkey or item_to_place.bigkey: + location.item = None + return None + + def valid_key_placement(item, location, itempool, world): if not valid_reserved_placement(item, location, world): return False @@ -259,6 +172,17 @@ def valid_reserved_placement(item, location, world): return True +def valid_dungeon_placement(item, location, world): + if location.parent_region.dungeon: + layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + if not is_dungeon_item(item, world) or item.player != location.player: + return layout.free_items > 0 + else: + # the second half probably doesn't matter much - should always return true + return item.dungeon == location.parent_region.dungeon.name and layout.dungeon_items > 0 + return not is_dungeon_item(item, world) + + def track_outside_keys(item, location, world): if not item.smallkey: return @@ -270,6 +194,22 @@ def track_outside_keys(item, location, world): world.key_logic[item.player][item_dungeon].outside_keys += 1 +def track_dungeon_items(item, location, world): + if location.parent_region.dungeon and not item.crystal: + 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 + + +def is_dungeon_item(item, world): + return ((item.smallkey and not world.keyshuffle[item.player]) + 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])) + + def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -351,6 +291,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) # get items to distribute + classify_major_items(world) random.shuffle(world.itempool) progitempool = [item for item in world.itempool if item.advancement] prioitempool = [item for item in world.itempool if not item.advancement and item.priority] @@ -379,21 +320,53 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # todo: crossed progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) + keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)} + if world.algorithm in ['balanced', 'equitable', 'vanilla_bias', 'dungeon_bias', 'entangled']: + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + random.shuffle(fill_locations) + if world.algorithm == 'balanced': + fast_fill(world, prioitempool, fill_locations) + elif world.algorithm == 'vanilla_bias': + fast_vanilla_fill(world, prioitempool, fill_locations) + elif world.algorithm in ['dungeon_bias', 'entangled']: + filtered_fill(world, prioitempool, fill_locations) + else: # just need to ensure dungeon items still get placed in dungeons + fast_equitable_fill(world, prioitempool, fill_locations) + # placeholder work + if world.algorithm == 'entangled' and world.players > 1: + random.shuffle(fill_locations) + placeholder_locations = filter_locations('Placeholder', fill_locations, world) + placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + for i in placeholder_items: + restitempool.remove(i) + for l in placeholder_locations: + fill_locations.remove(l) + filtered_fill(world, placeholder_items, placeholder_locations) + else: + primary, secondary = split_pool(progitempool, world) + fill_restrictive(world, world.state, fill_locations, primary, keys_in_pool, False, secondary) + random.shuffle(fill_locations) + tertiary, quaternary = split_pool(prioitempool, world) + prioitempool = [] + filtered_equitable_fill(world, tertiary, fill_locations) + prioitempool += tertiary + random.shuffle(fill_locations) + fill_restrictive(world, world.state, fill_locations, secondary, keys_in_pool) + random.shuffle(fill_locations) + fast_equitable_fill(world, quaternary, fill_locations) + prioitempool += quaternary - fill_restrictive(world, world.state, fill_locations, progitempool, - keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players + 1)}) - - random.shuffle(fill_locations) - - fast_fill(world, prioitempool, fill_locations) - - fast_fill(world, restitempool, fill_locations) + if world.algorithm == 'vanilla_bias': + fast_vanilla_fill(world, restitempool, fill_locations) + else: + fast_fill(world, restitempool, fill_locations) unplaced = [item.name for item in prioitempool + restitempool] unfilled = [location.name for location in fill_locations] if unplaced or unfilled: logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) + def fast_fill(world, item_pool, fill_locations): while item_pool and fill_locations: spot_to_fill = fill_locations.pop() @@ -401,70 +374,48 @@ def fast_fill(world, item_pool, fill_locations): world.push_item(spot_to_fill, item_to_place, False) -def flood_items(world): - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - progress_done = False +def filtered_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + item_locations = filter_locations(item_to_place, fill_locations, world) + spot_to_fill = next(iter(item_locations)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) # sweep once to pick up preplaced items world.state.sweep_for_events() - # fill world from top of itempool while we can - while not progress_done: - location_list = world.get_unfilled_locations() - random.shuffle(location_list) - spot_to_fill = None - for location in location_list: - if location.can_fill(world.state, itempool[0]): - spot_to_fill = location - break +def fast_vanilla_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world))) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) - if spot_to_fill: - item = itempool.pop(0) - world.push_item(spot_to_fill, item, True) - continue - # ran out of spots, check if we need to step in and correct things - if len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue +def filtered_equitable_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + item_locations = filter_locations(item_to_place, fill_locations, world) + spot_to_fill = next(l for l in item_locations if valid_dungeon_placement(item_to_place, l, world)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + track_dungeon_items(item_to_place, spot_to_fill, world) - # need to place a progress item instead of an already placed item, find candidate - item_to_place = None - candidate_item_to_place = None - for item in itempool: - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if item_to_place is None: - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - raise FillError('No more progress items left to place.') - - # find item to replace with progress item - location_list = world.get_reachable_locations() - random.shuffle(location_list) - for location in location_list: - if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey: - # safe to replace - replace_item = location.item - replace_item.location = None - itempool.append(replace_item) - world.push_item(location, item_to_place, True) - itempool.remove(item_to_place) - break +def fast_equitable_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + spot_to_fill = next(l for l in fill_locations if valid_dungeon_placement(item_to_place, l, world)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + track_dungeon_items(item_to_place, spot_to_fill, world) def lock_shop_locations(world, player): for shop, loc_names in shop_to_location_table.items(): for loc in loc_names: - world.get_location(loc, player).event = True + # world.get_location(loc, player).event = True world.get_location(loc, player).locked = True # I don't believe these locations exist in non-shopsanity # if world.retro[player]: diff --git a/ItemList.py b/ItemList.py index 5f5b5e7e..5721aadc 100644 --- a/ItemList.py +++ b/ItemList.py @@ -5,12 +5,13 @@ import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState from Bosses import place_bosses -from Dungeons import get_dungeon_item_pool from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location -from Fill import FillError, fill_restrictive, fast_fill +from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool from Items import ItemFactory +from source.item.BiasedFill import trash_items + import source.classes.constants as CONST @@ -262,8 +263,12 @@ def generate_itempool(world, player): if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] if amt < 0: - for _ in range(amt, 0): - pool.remove(next(iter([x for x in pool if x in ['Rupees (20)', 'Rupees (5)', 'Rupee (1)']]))) + trash_options = [x for x in pool if x in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x], reverse=True) + while amt > 0 and len(trash_options) > 0: + pool.remove(trash_options.pop()) + amt -= 1 elif amt > 0: for _ in range(0, amt): pool.append('Rupees (20)') diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5c65dbe6..67654c8c 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -177,6 +177,8 @@ class PlacementRule(object): return True available_keys = outside_keys empty_chests = 0 + # todo: sometimes we need an extra empty chest to accomodate the big key too + # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk for loc in check_locations: if not loc.item: diff --git a/Main.py b/Main.py index f6a94ce9..912ca72f 100644 --- a/Main.py +++ b/Main.py @@ -19,16 +19,17 @@ from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors -from DoorShuffle import link_doors, connect_portal +from DoorShuffle import link_doors, connect_portal, link_doors_prep from RoomData import create_rooms from Rules import set_rules -from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive -from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items +from Dungeons import create_dungeons +from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -from source.item.FillUtil import create_item_pool_config +from source.item.BiasedFill import create_item_pool_config, massage_item_pool + __version__ = '0.5.1.0-u' @@ -149,7 +150,6 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) - create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) @@ -165,7 +165,13 @@ def main(args, seed=None, fish=None): else: link_inverted_entrances(world, player) - logger.info(world.fish.translate("cli","cli","shuffling.dungeons")) + logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) + for player in range(1, world.players + 1): + link_doors_prep(world, player) + + create_item_pool_config(world) + + logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons")) for player in range(1, world.players + 1): link_doors(world, player) @@ -173,8 +179,7 @@ def main(args, seed=None, fish=None): mark_light_world_regions(world, player) else: mark_dark_world_regions(world, player) - logger.info(world.fish.translate("cli","cli","generating.itempool")) - logger.info(world.fish.translate("cli","cli","generating.itempool")) + logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): generate_itempool(world, player) @@ -192,8 +197,8 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) - - logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) + massage_item_pool(world) + logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) fill_prizes(world) @@ -202,14 +207,14 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) - shuffled_locations = None - if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + - list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())): + if args.algorithm in ['balanced', 'dungeon_bias', 'entangled']: shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) + elif args.algorithm == 'equitable': + promote_dungeon_items(world) else: - fill_dungeons(world) + promote_dungeon_items(world) for player in range(1, world.players+1): if world.logic[player] != 'nologic': @@ -227,34 +232,22 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","fill.world")) - if args.algorithm == 'flood': - flood_items(world) # different algo, biased towards early game progress items - elif args.algorithm == 'vt21': - distribute_items_cutoff(world, 1) - elif args.algorithm == 'vt22': - distribute_items_cutoff(world, 0.66) - elif args.algorithm == 'freshness': - distribute_items_staleness(world) - elif args.algorithm == 'vt25': - distribute_items_restrictive(world, False) - elif args.algorithm == 'vt26': - - distribute_items_restrictive(world, True, shuffled_locations) - elif args.algorithm == 'balanced': - distribute_items_restrictive(world, True) + distribute_items_restrictive(world, True) if world.players > 1: - logger.info(world.fish.translate("cli","cli","balance.multiworld")) - balance_multiworld_progression(world) + logger.info(world.fish.translate("cli", "cli", "balance.multiworld")) + if args.algorithm in ['balanced', 'equitable']: + balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom if not world.can_beat_game(log_error=True): - raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) + raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game")) for player in range(1, world.players+1): if world.shopsanity[player]: customize_shops(world, player) - balance_money_progression(world) + if args.algorithm in ['balanced', 'equitable']: + balance_money_progression(world) outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -405,6 +398,7 @@ def copy_world(world): ret.keydropshuffle = world.keydropshuffle.copy() ret.mixed_travel = world.mixed_travel.copy() ret.standardize_palettes = world.standardize_palettes.copy() + ret.restrict_boss_items = world.restrict_boss_items.copy() ret.exp_cache = world.exp_cache.copy() @@ -579,11 +573,11 @@ def create_playthrough(world): # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic # world.clear_exp_cache() if world.can_beat_game(state_cache[num]): - # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') + logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.add(location) else: # still required, got to keep it around - # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') + logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end diff --git a/Mystery.py b/Mystery.py index a53fb514..bf5585b5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -71,6 +71,8 @@ def main(): if args.enemizercli: erargs.enemizercli = args.enemizercli + mw_settings = {'algorithm': False} + settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} for player in range(1, args.multi + 1): @@ -79,7 +81,12 @@ def main(): settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) for k, v in vars(settings).items(): if v is not None: - getattr(erargs, k)[player] = v + if k == 'algorithm': # multiworld wide parameters + if not mw_settings[k]: # only use the first roll + setattr(erargs, k, v) + mw_settings[k] = True + else: + getattr(erargs, k)[player] = v else: raise RuntimeError(f'No weights specified for player {player}') @@ -116,6 +123,8 @@ def roll_settings(weights): ret = argparse.Namespace() + ret.algorithm = get_choice('algorithm') + glitches_required = get_choice('glitches_required') if glitches_required not in ['none', 'no_logic']: print("Only NMG and No Logic supported") diff --git a/Regions.py b/Regions.py index 35a7eda3..f26bbdb4 100644 --- a/Regions.py +++ b/Regions.py @@ -999,6 +999,14 @@ def adjust_locations(world, player): world.get_location(location, player).address = 0x400000 + index # player address? it is in the shop table index += 1 + # unreal events: + for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Dark Blacksmith Ruins', 'Frog', 'Missing Smith', 'Floodgate', + 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Attic Cracked Floor', 'Suspicious Maiden', + 'Revealing Light', 'Ice Block Drop', 'Zelda Pickup', 'Zelda Drop Off']: + location = world.get_location_unsafe(l, player) + if location: + location.real = False + # (type, room_id, shopkeeper, custom, locked, [items]) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index da496104..99ddec22 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -101,12 +101,12 @@ "algorithm": { "choices": [ "balanced", - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26" + "equitable", + "vanilla_bias", + "major_bias", + "dungeon_bias", + "cluster_bias", + "entangled" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 13c99b2a..7b7bb4f7 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -7,6 +7,7 @@ "seed": "Seed", "player": "Player", "shuffling.world": "Shuffling the World about", + "shuffling.prep": "Dungeon and Item prep", "shuffling.dungeons": "Shuffling dungeons", "shuffling.pots": "Shuffling pots", "basic.traversal": "--Basic Traversal", @@ -153,22 +154,29 @@ "balanced: vt26 derivative that aims to strike a balance between", " the overworld heavy vt25 and the dungeon heavy vt26", " algorithm.", - "vt26: Shuffle items and place them in a random location", - " that it is not impossible to be in. This includes", - " dungeon keys and items.", - "vt25: Shuffle items and place them in a random location", - " that it is not impossible to be in.", - "vt21: Unbiased in its selection, but has tendency to put", - " Ice Rod in Turtle Rock.", - "vt22: Drops off stale locations after 1/3 of progress", - " items were placed to try to circumvent vt21\\'s", - " shortcomings.", - "Freshness: Keep track of stale locations (ones that cannot be", - " reached yet) and decrease likeliness of selecting", - " them the more often they were found unreachable.", - "Flood: Push out items starting from Link\\'s House and", - " slightly biased to placing progression items with", - " less restrictions." + "equitable: does not place dungeon items first allowing new potential", + " but mixed with the normal advancement pool", + "biased placements: these consider all major items to be special and attempts", + "to place items from fixed to semi-random locations. For purposes of these shuffles, all", + "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", + "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool", + "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),", + "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and", + "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.", + "vanilla_bias Same as above, but attempts to place items in their vanilla", + " location first. Major items that cannot be placed that way", + " will attempt to be placed in other failed locations first.", + " Also attempts to place junk items in vanilla locations", + "major_bias same as above, but uses the major items' location preferentially", + " major item location are defined as the group of location where", + " the items are found in the vanilla game. Backup locations for items", + " not in the vanilla game will be in the documentation", + "dungeon_bias same as above, but major items are preferentially placed", + " in dungeons locations first", + "cluster_bias same as above, but groups of locations are chosen randomly", + " from a pool of fixed locations designed to be interesting", + " and give major clues about the location of other", + " advancement items. These fixed groups will be documented" ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index ae26b0dd..4cfeaafb 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -279,13 +279,12 @@ "randomizer.item.accessibility.none": "Beatable", "randomizer.item.sortingalgo": "Item Sorting", - "randomizer.item.sortingalgo.freshness": "Freshness", - "randomizer.item.sortingalgo.flood": "Flood", - "randomizer.item.sortingalgo.vt21": "VT8.21", - "randomizer.item.sortingalgo.vt22": "VT8.22", - "randomizer.item.sortingalgo.vt25": "VT8.25", - "randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.balanced": "Balanced", + "randomizer.item.sortingalgo.equitable": "Equitable", + "randomizer.item.sortingalgo.vanilla_bias": "Biased: Vanilla", + "randomizer.item.sortingalgo.major_bias": "Biased: Major Items", + "randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons", + "randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered", "randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items.none": "None", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 89cacb00..7f524a33 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -116,13 +116,12 @@ "type": "selectbox", "default": "balanced", "options": [ - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26", - "balanced" + "balanced", + "equitable", + "vanilla_bias", + "major_bias", + "dungeon_bias", + "cluster_bias" ] }, "restrict_boss_items": { diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py new file mode 100644 index 00000000..45301093 --- /dev/null +++ b/source/item/BiasedFill.py @@ -0,0 +1,881 @@ +import RaceRandom as random +import logging +from collections import defaultdict + +from DoorShuffle import validate_vanilla_reservation +from Dungeons import dungeon_table +from Items import item_table, ItemFactory + + +class ItemPoolConfig(object): + + def __init__(self): + self.location_groups = None + self.static_placement = None + self.item_pool = None + self.placeholders = None + self.reserved_locations = defaultdict(set) + + +class LocationGroup(object): + def __init__(self, name): + self.name = name + self.locations = [] + + # flags + self.keyshuffle = False + self.keydropshuffle = False + self.shopsanity = False + self.retro = False + + def locs(self, locs): + self.locations = locs + return self + + def flags(self, k, d=False, s=False, r=False): + self.keyshuffle = k + self.keydropshuffle = d + self.shopsanity = s + self.retro = r + return self + + +def create_item_pool_config(world): + world.item_pool_config = config = ItemPoolConfig() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True + if world.algorithm == 'vanilla_bias': + config.static_placement = {} + config.location_groups = {} + for player in range(1, world.players + 1): + config.static_placement[player] = vanilla_mapping.copy() + if world.keydropshuffle[player]: + for item, locs in keydrop_vanilla_mapping.items(): + if item in config.static_placement[player]: + config.static_placement[player][item].extend(locs) + else: + config.static_placement[player][item] = list(locs) + # todo: shopsanity... + # todo: retro (universal keys...) + # retro + shops + config.location_groups[player] = [ + LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), + LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), + LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] + elif world.algorithm == 'major_bias': + config.location_groups = [ + LocationGroup('MajorItems'), + LocationGroup('Backup') + ] + config.item_pool = {} + 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.bigkeyshuffle[player]: + groups.locations.extend(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + groups.locations.append(mode_grouping['Big Key Drops']) + if world.keyshuffle[player]: + groups.locations.extend(mode_grouping['Small Keys']) + if world.keydropshuffle[player]: + groups.locations.extend(mode_grouping['Key Drops']) + if world.compassshuffle[player]: + groups.locations.extend(mode_grouping['Compasses']) + if world.mapshuffle[player]: + groups.locations.extend(mode_grouping['Maps']) + if world.shopsanity[player]: + groups.locations.append('Capacity Upgrade - Left') + groups.locations.append('Capacity Upgrade - Right') + if world.retro[player]: + if world.shopsanity[player]: + pass # todo: 5 locations for single arrow representation? + config.item_pool[player] = determine_major_items(world, player) + config.location_groups[0].locations = set(groups.locations) + backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) + config.location_groups[1].locations = set(backup) + elif world.algorithm == 'dungeon_bias': + config.location_groups = [ + LocationGroup('Dungeons'), + LocationGroup('Backup') + ] + config.item_pool = {} + dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + config.location_groups[0].locations = set(dungeon_set) + backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major'] + + mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops']) + config.location_groups[1].locations = set(backup) + elif world.algorithm == 'entangled' and world.players > 1: + config.location_groups = [ + LocationGroup('Entangled'), + ] + item_cnt = 0 + config.item_pool = {} + limits = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + limits[player] = calc_dungeon_limits(world, player) + c_set = {} + for location in world.get_locations(): + if location.real and not location.forced_item: + c_set[location.name] = None + # todo: retroshop locations are created later, so count them here? + ttl_locations, candidates = 0, list(c_set.keys()) + chosen_locations = defaultdict(set) + random.shuffle(candidates) + while ttl_locations < item_cnt: + choice = candidates.pop() + dungeon = world.get_location(choice, 1).parent_region.dungeon + if dungeon: + for player in range(1, world.players + 1): + location = world.get_location(choice, player) + if location.real and not location.forced_item: + if isinstance(limits[player], int): + if limits[player] > 0: + config.reserved_locations[player].add(choice) + limits[player] -= 1 + chosen_locations[choice].add(player) + else: + previous = previously_reserved(location, world, player) + if limits[player][dungeon.name] > 0 or previous: + if validate_reservation(location, dungeon, world, player): + if not previous: + limits[player][dungeon.name] -= 1 + chosen_locations[choice].add(player) + else: # not dungeon restricted + for player in range(1, world.players + 1): + location = world.get_location(choice, player) + if location.real and not location.forced_item: + chosen_locations[choice].add(player) + ttl_locations += len(chosen_locations[choice]) + config.placeholders = ttl_locations - item_cnt + config.location_groups[0].locations = chosen_locations + + +def previously_reserved(location, world, player): + if '- Boss' 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 not (world.keyshuffle[player] or world.retro[player])): + return True + return False + + +def massage_item_pool(world): + player_pool = defaultdict(list) + for item in world.itempool: + player_pool[item.player].append(item) + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item not in player_pool[item.player]: # filters out maps, compasses, etc + 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] + 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] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + while discrepancy > 0 and len(trash_options) > 0: + deleted = trash_options.pop() + world.itempool.remove(deleted) + discrepancy -= 1 + if discrepancy > 0: + logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random') + if world.item_pool_config.placeholders is not None: + removed = 0 + single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)'] + removed += len(single_rupees) + for x in single_rupees: + world.itempool.remove(x) + if removed < world.item_pool_config.placeholders: + trash_options = [x for x in world.itempool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + while removed < world.item_pool_config.placeholders: + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + world.itempool.remove(deleted) + removed += 1 + placeholders = random.sample(single_rupees, world.item_pool_config.placeholders) + world.itempool += placeholders + removed -= len(placeholders) + for _ in range(removed): + world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players))) + + +def validate_reservation(location, dungeon, world, player): + world.item_pool_config.reserved_locations[player].add(location.name) + if world.doorShuffle[player] != 'vanilla': + return True # we can generate the dungeon somehow most likely + if validate_vanilla_reservation(dungeon, world, player): + return True + world.item_pool_config.reserved_locations[player].remove(location.name) + return False + + +def count_major_items(world, player): + major_item_set = 52 + if world.bigkeyshuffle[player]: + major_item_set += 11 + if world.keydropshuffle[player]: + major_item_set += 1 + if world.doorShuffle[player] == 'crossed': + major_item_set += 1 + if world.keyshuffle[player]: + major_item_set += 29 + if world.keydropshuffle[player]: + major_item_set += 32 + if world.compassshuffle[player]: + major_item_set += 11 + if world.doorShuffle[player] == 'crossed': + major_item_set += 2 + if world.mapshuffle[player]: + major_item_set += 12 + if world.doorShuffle[player] == 'crossed': + major_item_set += 1 + if world.shopsanity[player]: + major_item_set += 2 + if world.retro[player]: + major_item_set += 5 # the single arrow quiver + if world.goal == 'triforcehunt': + major_item_set += world.triforce_pool[player] + if world.bombbag[player]: + major_item_set += world.triforce_pool[player] + # todo: vanilla, assured, swordless? + # if world.swords[player] != "random": + # if world.swords[player] == 'assured': + # major_item_set -= 1 + # if world.swords[player] in ['vanilla', 'swordless']: + # major_item_set -= 4 + # todo: starting equipment? + return major_item_set + + +def calc_dungeon_limits(world, player): + b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player], + world.mapshuffle[player], world.keydropshuffle[player], world.retro[player], + world.restrict_boss_items[player]) + if world.doorShuffle[player] in ['vanilla', 'basic']: + limits = {} + for dungeon, info in dungeon_table.items(): + val = info.free_items + if bi != 'none' and info.prize: + if bi == 'mapcompass' and (not c or not m): + val -= 1 + if bi == 'dungeon' and (not c or not m or not (s or r) or not b): + val -= 1 + if b: + val += 1 if info.bk_present else 0 + if k: + val += 1 if info.bk_drops else 0 + if s or r: + val += info.key_num + if k: + val += info.key_drops + if c: + val += 1 if info.compass_present else 0 + if m: + val += 1 if info.map_present else 0 + limits[dungeon] = val + else: + limits = 60 + if world.bigkeyshuffle[player]: + limits += 11 + if world.keydropshuffle[player]: + limits += 1 + if world.keyshuffle[player] or world.retro[player]: + limits += 29 + if world.keydropshuffle[player]: + limits += 32 + if world.compassshuffle[player]: + limits += 11 + if world.mapshuffle[player]: + limits += 12 + return limits + + +def determine_major_items(world, player): + major_item_set = set(major_items) + if world.bigkeyshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) + if world.keyshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'}) + if world.compassshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'}) + if world.mapshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'}) + if world.shopsanity[player]: + major_item_set.add('Bomb Upgrade (+5)') + major_item_set.add('Arrow Upgrade (+5)') + if world.retro[player]: + major_item_set.add('Single Arrow') + major_item_set.add('Small Key (Universal)') + if world.goal == 'triforcehunt': + major_item_set.add('Triforce Piece') + if world.bombbag[player]: + major_item_set.add('Bomb Upgrade (+10)') + return major_item_set + + +def classify_major_items(world): + if world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled' + and world.players > 1): + config = world.item_pool_config + for item in world.itempool: + if item.name in config.item_pool[item.player]: + if not item.advancement or not item.priority: + if item.smallkey or item.bigkey: + item.advancement = True + else: + item.priority = True + + +def split_pool(pool, world): + # bias or entangled + config = world.item_pool_config + priority, secondary = [], [] + for item in pool: + if item.name in config.item_pool[item.player]: + priority.append(item) + else: + secondary.append(item) + return priority, secondary + + +def filter_locations(item_to_place, locations, world): + if world.algorithm == 'vanilla_bias': + config, filtered = world.item_pool_config, [] + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + if item_name in config.static_placement[item_to_place.player]: + restricted = config.static_placement[item_to_place.player][item_name] + filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + i = 0 + while len(filtered) <= 0: + if i >= len(config.location_groups[item_to_place.player]): + return locations + restricted = config.location_groups[item_to_place.player][i].locations + filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + i += 1 + return filtered + if world.algorithm in ['major_bias', 'dungeon_bias']: + config = world.item_pool_config + 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: + restricted = config.location_groups[1].locations + filtered = [l for l in locations if l.name in restricted] + # bias toward certain location in overflow? (thinking about this for major_bias) + return filtered if len(filtered) > 0 else locations + if world.algorithm == 'entangled' and world.players > 1: + config = world.item_pool_config + if item_to_place == 'Placeholder' or 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 and l.player in restricted[l.name]] + return filtered if len(filtered) > 0 else locations + return locations + + +vanilla_mapping = { + 'Green Pendant': ['Eastern Palace - Prize'], + 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], + 'Blue Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], + 'Crystal 1': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 2': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 3': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 4': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 7': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'], + 'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'], + 'Bow': ['Eastern Palace - Big Chest'], + 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Left'], + 'Book of Mudora': ['Library'], + 'Hammer': ['Palace of Darkness - Big Chest'], + 'Hookshot': ['Swamp Palace - Big Chest'], + 'Magic Mirror': ['Old Man'], + 'Ocarina': ['Flute Spot'], + 'Pegasus Boots': ['Sahasrahla'], + 'Power Glove': ['Desert Palace - Big Chest'], + 'Cape': ["King's Tomb"], + 'Mushroom': ['Mushroom'], + 'Shovel': ['Stumpy'], + 'Lamp': ["Link's House"], + 'Magic Powder': ['Potion Shop'], + 'Moon Pearl': ['Tower of Hera - Big Chest'], + 'Cane of Somaria': ['Misery Mire - Big Chest'], + 'Fire Rod': ['Skull Woods - Big Chest'], + 'Flippers': ['King Zora'], + 'Ice Rod': ['Ice Rod Cave'], + 'Titans Mitts': ["Thieves' Town - Big Chest"], + 'Bombos': ['Bombos Tablet'], + 'Ether': ['Ether Tablet'], + 'Quake': ['Catfish'], + 'Bottle': ['Bottle Merchant', 'Kakariko Tavern', 'Purple Chest', 'Hobo'], + 'Master Sword': ['Master Sword Pedestal'], + 'Tempered Sword': ['Blacksmith'], + 'Fighter Sword': ["Link's Uncle"], + 'Golden Sword': ['Pyramid Fairy - Right'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Right'], + 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], + 'Silver Arrows': ['Pyramid Fairy - Left'], + 'Single Arrow': ['Palace of Darkness - Dark Basement - Left'], + 'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right', + 'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left'], + 'Bombs (3)': ['Floodgate Chest', "Sahasrahla's Hut - Middle", 'Kakariko Well - Bottom', 'Superbunny Cave - Top', + 'Mini Moldorm Cave - Far Left', 'Sewers - Secret Room - Left', 'Paradox Cave Upper - Left', + "Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Maze - Top', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right'], + 'Blue Mail': ['Ice Palace - Big Chest'], + 'Red Mail': ['Ganons Tower - Big Chest'], + 'Progressive Armor': ['Ice Palace - Big Chest', 'Ganons Tower - Big Chest'], + 'Blue Boomerang': ['Hyrule Castle - Boomerang Chest'], + 'Red Boomerang': ['Waterfall Fairy - Left'], + 'Blue Shield': ['Secret Passage'], + 'Red Shield': ['Waterfall Fairy - Right'], + 'Mirror Shield': ['Turtle Rock - Big Chest'], + 'Progressive Shield': ['Secret Passage', 'Waterfall Fairy - Right', 'Turtle Rock - Big Chest'], + 'Bug Catching Net': ['Sick Kid'], + 'Cane of Byrna': ['Spike Cave'], + 'Boss Heart Container': ['Desert Palace - Boss', 'Eastern Palace - Boss', 'Tower of Hera - Boss', + 'Swamp Palace - Boss', "Thieves' Town - Boss", 'Skull Woods - Boss', 'Ice Palace - Boss', + 'Misery Mire - Boss', 'Turtle Rock - Boss', 'Palace of Darkness - Boss'], + 'Sanctuary Heart Container': ['Sanctuary'], + 'Piece of Heart': ['Sunken Treasure', "Blind's Hideout - Top", "Zora's Ledge", "Aginah's Cave", 'Maze Race', + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Lumberjack Tree', 'Cave 45', 'Graveyard Cave', + 'Checkerboard Cave', 'Bonk Rock Cave', 'Lake Hylia Island', 'Desert Ledge', 'Spectacle Rock', + 'Spectacle Rock Cave', 'Pyramid', 'Digging Game', 'Peg Cave', 'Chest Game', 'Bumper Cave Ledge', + 'Mire Shed - Left', 'Floating Island', 'Mimic Cave'], + 'Rupee (1)': ['Turtle Rock - Eye Bridge - Top Right', 'Ganons Tower - Compass Room - Top Right'], + 'Rupees (5)': ["Hyrule Castle - Zelda's Chest", 'Turtle Rock - Eye Bridge - Top Left', + # 'Palace of Darkness - Harmless Hellway', + 'Palace of Darkness - Dark Maze - Bottom', + 'Ganons Tower - Validation Chest'], + 'Rupees (20)': ["Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', + 'Kakariko Well - Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', 'Hype Cave - Top', + 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', + 'Swamp Palace - West Chest', 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Waterfall Room', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", + 'Turtle Rock - Eye Bridge - Bottom Right', 'Ganons Tower - Compass Room - Bottom Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], + 'Rupees (50)': ["Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", 'Spiral Cave', 'Superbunny Cave - Bottom', + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', + 'Hookshot Cave - Bottom Left'], + 'Rupees (100)': ['Eastern Palace - Cannonball Chest'], + 'Rupees (300)': ['Mini Moldorm Cave - Generous Guy', 'Sewers - Secret Room - Middle', 'Hype Cave - Generous Guy', + 'Brewery', 'C-Shaped House'], + 'Magic Upgrade (1/2)': ['Magic Bat'], + 'Big Key (Eastern Palace)': ['Eastern Palace - Big Key Chest'], + 'Compass (Eastern Palace)': ['Eastern Palace - Compass Chest'], + 'Map (Eastern Palace)': ['Eastern Palace - Map Chest'], + 'Small Key (Desert Palace)': ['Desert Palace - Torch'], + 'Big Key (Desert Palace)': ['Desert Palace - Big Key Chest'], + 'Compass (Desert Palace)': ['Desert Palace - Compass Chest'], + 'Map (Desert Palace)': ['Desert Palace - Map Chest'], + 'Small Key (Tower of Hera)': ['Tower of Hera - Basement Cage'], + 'Big Key (Tower of Hera)': ['Tower of Hera - Big Key Chest'], + 'Compass (Tower of Hera)': ['Tower of Hera - Compass Chest'], + 'Map (Tower of Hera)': ['Tower of Hera - Map Chest'], + 'Small Key (Escape)': ['Sewers - Dark Cross'], + 'Map (Escape)': ['Hyrule Castle - Map Chest'], + 'Small Key (Agahnims Tower)': ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], + 'Small Key (Palace of Darkness)': ['Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', + 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - The Arena - Ledge', + 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Harmless Hellway'], + # 'Palace of Darkness - Dark Maze - Bottom'], + 'Big Key (Palace of Darkness)': ['Palace of Darkness - Big Key Chest'], + 'Compass (Palace of Darkness)': ['Palace of Darkness - Compass Chest'], + 'Map (Palace of Darkness)': ['Palace of Darkness - Map Chest'], + 'Small Key (Thieves Town)': ["Thieves' Town - Blind's Cell"], + 'Big Key (Thieves Town)': ["Thieves' Town - Big Key Chest"], + 'Compass (Thieves Town)': ["Thieves' Town - Compass Chest"], + 'Map (Thieves Town)': ["Thieves' Town - Map Chest"], + 'Small Key (Skull Woods)': ['Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Skull Woods - Bridge Room'], + 'Big Key (Skull Woods)': ['Skull Woods - Big Key Chest'], + 'Compass (Skull Woods)': ['Skull Woods - Compass Chest'], + 'Map (Skull Woods)': ['Skull Woods - Map Chest'], + 'Small Key (Swamp Palace)': ['Swamp Palace - Entrance'], + 'Big Key (Swamp Palace)': ['Swamp Palace - Big Key Chest'], + 'Compass (Swamp Palace)': ['Swamp Palace - Compass Chest'], + 'Map (Swamp Palace)': ['Swamp Palace - Map Chest'], + 'Small Key (Ice Palace)': ['Ice Palace - Iced T Room', 'Ice Palace - Spike Room'], + 'Big Key (Ice Palace)': ['Ice Palace - Big Key Chest'], + 'Compass (Ice Palace)': ['Ice Palace - Compass Chest'], + 'Map (Ice Palace)': ['Ice Palace - Map Chest'], + 'Small Key (Misery Mire)': ['Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], + 'Big Key (Misery Mire)': ['Misery Mire - Big Key Chest'], + 'Compass (Misery Mire)': ['Misery Mire - Compass Chest'], + 'Map (Misery Mire)': ['Misery Mire - Map Chest'], + 'Small Key (Turtle Rock)': ['Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', + 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left'], + 'Big Key (Turtle Rock)': ['Turtle Rock - Big Key Chest'], + 'Compass (Turtle Rock)': ['Turtle Rock - Compass Chest'], + 'Map (Turtle Rock)': ['Turtle Rock - Roller Room - Left'], + 'Small Key (Ganons Tower)': ["Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'Big Key (Ganons Tower)': ['Ganons Tower - Big Key Chest'], + 'Compass (Ganons Tower)': ['Ganons Tower - Compass Room - Top Left'], + 'Map (Ganons Tower)': ['Ganons Tower - Map Chest'] +} + + +keydrop_vanilla_mapping = { + 'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'], + 'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'], + 'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop'], + 'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'], + 'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], + 'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"], + 'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'], + 'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key'], + 'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'], + 'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], +} + +mode_grouping = { + 'Overworld Major': [ + "Link's Uncle", 'King Zora', "Link's House", 'Sahasrahla', 'Ice Rod Cave', 'Library', + 'Master Sword Pedestal', 'Old Man', 'Ether Tablet', 'Catfish', 'Stumpy', 'Bombos Tablet', 'Mushroom', + 'Bottle Merchant', 'Kakariko Tavern', 'Secret Passage', 'Flute Spot', 'Purple Chest', + 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', 'Magic Bat', 'Sick Kid', 'Hobo', + 'Potion Shop', 'Spike Cave', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', "King's Tomb", + ], + 'Big Chests': ['Eastern Palace - Big Chest','Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest'], + 'Heart Containers': ['Sanctuary', 'Eastern Palace - Boss','Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', + "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss'], + 'Heart Pieces': [ + 'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island', + 'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree', + 'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45', + 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", + 'Chest Game', 'Digging Game', 'Mire Shed - Right', 'Mimic 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', + 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', + 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', + ], + 'Compasses': [ + 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', + 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', + "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', + 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left' + ], + 'Maps': [ + 'Hyrule Castle - Map Chest', 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', + 'Tower of Hera - Map Chest', 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', + 'Skull Woods - Map Chest', "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', + 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest' + ], + 'Small Keys': [ + 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - The Arena - Ledge', + "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Misery Mire - Spike Chest', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Swamp Palace - Entrance', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Roller Room - Right', + 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest' + ], + 'Dungeon Trash': [ + 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + "Hyrule Castle - Zelda's Chest", 'Eastern Palace - Cannonball Chest', "Thieves' Town - Ambush Chest", + "Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Basement - Left', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Dark Maze - Top', + 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterfall Room', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', 'Swamp Palace - West Chest', + ], + 'Overworld Trash': [ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', + 'Kakariko Well - Bottom', 'Chicken House', 'Floodgate Chest', 'Mini Moldorm Cave - Left', + 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', + 'Mini Moldorm Cave - Far Right', "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", + "Sahasrahla's Hut - Middle", 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', + 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House', + 'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', + 'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right', + 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Left' + ], + 'GT Trash': [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Randomizer Room - Bottom Left', "Ganons Tower - Bob's Chest", + 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', + 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', + 'Ganons Tower - Validation Chest', + ], + 'Key Drops': [ + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', + 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', + 'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key", + "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key', + 'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key', + 'Ganons Tower - Mini Helmasuar Key Drop', + ], + 'Big Key Drops': ['Hyrule Castle - Big Key Drop'], + 'Shops': [ + 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', + 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Dark Lake Hylia Shop - Left', + 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Dark Lumberjack Shop - Left', + 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', 'Village of Outcasts Shop - Left', + 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left', + 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle', + 'Paradox Shop - Right', 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Left', + 'Capacity Upgrade - Right' + ], + 'RetroShops': [ + 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', 'Take-Any #2 Item 1', + 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2','Take-Any #4 Item 1', 'Take-Any #4 Item 2' + ] +} + + +major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', + 'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', + 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', + 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', + 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Blue Shield', 'Red Shield', + 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', + 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', + 'Progressive Bow', 'Progressive Bow (Alt)'} + + +# todo: re-enter these +clustered_groups = [ + LocationGroup("MajorRoute1").locs([ + 'Library', 'Master Sword Pedestal', 'Old Man', 'Flute Spot', + 'Ether Tablet', 'Stumpy', 'Bombos Tablet', 'Mushroom', 'Bottle Merchant', 'Kakariko Tavern', + 'Sick Kid', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right' + ]), + LocationGroup("MajorRoute2").locs([ + 'King Zora', 'Sahasrahla', 'Ice Rod Cave', 'Catfish', + 'Purple Chest', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', + 'Magic Bat', 'Hobo', 'Potion Shop', 'Spike Cave', "King's Tomb" + ]), + LocationGroup("BigChest").locs([ + 'Sanctuary', 'Eastern Palace - Big Chest', + 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', 'Palace of Darkness - Big Chest', + 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', "Thieves' Town - Big Chest", + 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', 'Ice Palace - Big Chest', + 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest' + ]), + LocationGroup("BossUncle").locs([ + "Link's Uncle", "Link's House", 'Secret Passage', 'Eastern Palace - Boss', + 'Desert Palace - Boss', 'Tower of Hera - Boss', 'Palace of Darkness - Boss', 'Swamp Palace - Boss', + 'Skull Woods - Boss', "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', + 'Turtle Rock - Boss']), + LocationGroup("HeartPieces LW").locs([ + 'Lost Woods Hideout', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Maze Race', 'Sunken Treasure', + 'Bonk Rock Cave', 'Desert Ledge', "Aginah's Cave", 'Spectacle Rock Cave', 'Spectacle Rock', 'Pyramid', + 'Lumberjack Tree', "Zora's Ledge"]), + LocationGroup("HeartPieces DW").locs([ + 'Lake Hylia Island', 'Chest Game', 'Digging Game', 'Graveyard Cave', 'Mimic Cave', + 'Cave 45', 'Peg Cave', 'Bumper Cave Ledge', 'Checkerboard Cave', 'Mire Shed - Right', 'Floating Island', + 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right']), + LocationGroup("Minor Trash").locs([ + 'Ice Palace - Freezor Chest', 'Skull Woods - Pot Prison', 'Misery Mire - Bridge Chest', + 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Maze - Top', + 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', + 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + 'Swamp Palace - Waterfall Room', 'Turtle Rock - Eye Bridge - Bottom Right', + 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right']), + LocationGroup("CompassTT").locs([ + "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", + 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', + 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', + "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', + 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left']), + LocationGroup("Early SKs").locs([ + 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', + 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Harmless Hellway', + "Thieves' Town - Blind's Cell", 'Eastern Palace - Cannonball Chest', + 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', + 'Sewers - Secret Room - Middle', 'Floodgate Chest' + ]), + LocationGroup("Late SKs").locs([ + 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', "Hyrule Castle - Zelda's Chest", + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Swamp Palace - West Chest', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest', + ]), + LocationGroup("Kak-LDM").locs([ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Chicken House', 'Paradox Cave Lower - Far Left', + 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', + 'Paradox Cave Lower - Middle', 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', + ]), + LocationGroup("BK-Bunny").locs([ + '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', + 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', + 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', + 'Superbunny Cave - Top', 'Superbunny Cave - Bottom', + ]), + LocationGroup("Early Drops").flags(True, True).locs([ + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', + 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Hyrule Castle - Big Key Drop', + ]), + LocationGroup("Late Drops").flags(True, True).locs([ + 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', + 'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Ganons Tower - Conveyor Cross Pot Key', + 'Ganons Tower - Double Switch Pot Key']), + LocationGroup("SS-Hype-Voo").locs([ + 'Mini Moldorm Cave - Left', + 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', + 'Mini Moldorm Cave - Far Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', 'Brewery', + 'C-Shaped House', 'Palace of Darkness - The Arena - Ledge', + ]), + LocationGroup("DDM Hard").flags(True, True).locs([ + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', + 'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', + 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Roller Room - Right', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop' + ]), + LocationGroup("Kak Shop").flags(False, False, True).locs([ + 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', + 'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', + 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', + 'Capacity Upgrade - Left']), + LocationGroup("Hylia Shop").flags(False, False, True).locs([ + 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', + 'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', + 'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', + 'Capacity Upgrade - Right']), + LocationGroup("Map Validation").locs([ + 'Hyrule Castle - Map Chest', + 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', 'Tower of Hera - Map Chest', + 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', 'Skull Woods - Map Chest', + "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', + 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest', 'Ganons Tower - Validation Chest']), + LocationGroup("SahasWell+MireHopeDDMShop").flags(False, False, True).locs([ + 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', + 'Kakariko Well - Bottom', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Mire Shed - Left', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right']), + LocationGroup("Tower Pain").flags(True).locs([ + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Room - Left']), + LocationGroup("Retro Shops").flags(False, False, True, True).locs([ + 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', + 'Take-Any #2 Item 1', 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2', + 'Take-Any #4 Item 1', 'Take-Any #4 Item 2', 'Swamp Palace - Entrance', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Top Right', + 'Ganons Tower - Compass Room - Bottom Right', + ]) + +] + + +trash_items = { + 'Nothing': -1, + 'Bee Trap': 0, + 'Rupee (1)': 1, + 'Rupees (5)': 1, + 'Rupees (20)': 1, + + 'Small Heart': 2, + 'Bee': 2, + + 'Bombs (3)': 3, + 'Arrows (10)': 3, + 'Bombs (10)': 3, + + 'Red Potion': 4, + 'Blue Shield': 4, + 'Rupees (50)': 4, + 'Rupees (100)': 4, + 'Rupees (300)': 5, + + 'Piece of Heart': 17 +} diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py deleted file mode 100644 index 958c6b67..00000000 --- a/source/item/FillUtil.py +++ /dev/null @@ -1,20 +0,0 @@ -from collections import defaultdict - -from Dungeons import dungeon_prize - -class ItemPoolConfig(object): - - def __init__(self): - self.reserved_locations = defaultdict(set) - - -def create_item_pool_config(world): - config = ItemPoolConfig() - if world.algorithm in ['balanced']: - for player in range(1, world.players+1): - if world.restrict_boss_items[player]: - for dungeon in dungeon_prize: - if dungeon.startswith('Thieves'): - dungeon = "Thieves' Town" - config.reserved_locations[player].add(f'{dungeon} - Boss') - world.item_pool_config = config From 5c835dc243ebb9fde6faf9d1efa8424fa165fa13 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 008/293] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 45 ++++++++++++++++++++++++++++++++------------- KeyDoorShuffle.py | 10 +++++----- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3a2dc49b..b35e53da 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -441,8 +441,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index a37ce3d4..2edba0d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -373,6 +373,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -405,19 +437,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': "Thieves' Town - Prize", - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} - dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5c666e4..5c65dbe6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1378,7 +1378,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1541,7 +1541,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1966,8 +1966,8 @@ def validate_key_placement(key_layout, world, 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) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + 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) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 4e8a8d28406a1fd59737cd2dcc5b69ad95e27054 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:25:29 -0600 Subject: [PATCH 009/293] Compass/Map can be progressive Fixed filter_for_potential_bk_locations Changed rules to use dungeon_table --- BaseClasses.py | 8 ++++---- DungeonGenerator.py | 5 +++-- Rules.py | 11 ++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b35e53da..67f2f93d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -852,7 +852,7 @@ class CollectionState(object): reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item - if item_player == player and self.check_if_progressive(item_name): + if item_player == player and self.check_if_progressive(item_name, player): if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: @@ -868,8 +868,7 @@ class CollectionState(object): reduced[('Heart Container', player)] = 1 return frozenset(reduced.items()) - @staticmethod - def check_if_progressive(item_name): + def check_if_progressive(self, item_name, player): return (item_name in ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', @@ -881,7 +880,8 @@ class CollectionState(object): 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] - or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) + or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass')))) def can_reach(self, spot, resolution_hint=None, player=None): try: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 4be27692..a2c3df04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -679,7 +679,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return count_locations_exclude_big_chest(locations, world, player) + return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] type_map = { @@ -1078,7 +1079,7 @@ def prize_or_event(loc): def reserved_location(loc, world, player): - return loc.name in world.item_pool_config.reserved_locations[player] + return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player] def blind_boss_unavail(loc, locations, world, player): diff --git a/Rules.py b/Rules.py index f06328c5..dec41c79 100644 --- a/Rules.py +++ b/Rules.py @@ -4,7 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType -from Dungeons import dungeon_regions, dungeon_prize +from Dungeons import dungeon_table from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -562,10 +562,11 @@ def global_rules(world, player): map_name = f'Map ({d_name})' add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) - for dungeon in dungeon_prize.keys(): - d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon - for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: - add_mc_rule(loc) + for dungeon, info in dungeon_table.items(): + if info.prize: + 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': add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') From 23352c3bf7ad1098969d4209a41b339349399ed4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:36:12 -0600 Subject: [PATCH 010/293] Correct promotion of map and compass to advancement to add that logic --- source/item/FillUtil.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 958c6b67..6aa2f5cf 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -1,6 +1,6 @@ from collections import defaultdict -from Dungeons import dungeon_prize +from Dungeons import dungeon_table class ItemPoolConfig(object): @@ -9,12 +9,17 @@ class ItemPoolConfig(object): def create_item_pool_config(world): - config = ItemPoolConfig() - if world.algorithm in ['balanced']: - for player in range(1, world.players+1): - if world.restrict_boss_items[player]: - for dungeon in dungeon_prize: - if dungeon.startswith('Thieves'): - dungeon = "Thieves' Town" - config.reserved_locations[player].add(f'{dungeon} - Boss') - world.item_pool_config = config + world.item_pool_config = config = ItemPoolConfig() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True From ebf237cca3b642c1edcbce4693ee7bfe86e2f25a Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:00:55 -0600 Subject: [PATCH 011/293] Ambrosia logic fixes --- DungeonGenerator.py | 2 +- Fill.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 72938538..55a4b9ec 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -680,7 +680,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] diff --git a/Fill.py b/Fill.py index 65aa2f85..339a01ce 100644 --- a/Fill.py +++ b/Fill.py @@ -167,7 +167,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return item.is_inside_dungeon_item(world) + return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this def valid_reserved_placement(item, location, world): From 391db7b5c4ba35c9d51d8be090441ff80566ebe5 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:02:18 -0600 Subject: [PATCH 012/293] Clustered bias algorithm Fixes for various other algorithms --- DoorShuffle.py | 1 + Fill.py | 131 +++++++++---- Main.py | 4 +- source/item/BiasedFill.py | 399 ++++++++++++++++++++++++-------------- 4 files changed, 344 insertions(+), 191 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 8ee16a33..a7e2ee72 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -44,6 +44,7 @@ def link_doors(world, player): reset_rooms(world, player) world.get_door("Skull Pinball WS", player).no_exit() world.swamp_patch_required[player] = orig_swamp_patch + link_doors_prep(world, player) def link_doors_prep(world, player): diff --git a/Fill.py b/Fill.py index 339a01ce..872f09e7 100644 --- a/Fill.py +++ b/Fill.py @@ -6,7 +6,7 @@ import logging from BaseClasses import CollectionState, FillError from Items import ItemFactory from Regions import shop_to_location_table, retro_shops -from source.item.BiasedFill import filter_locations, classify_major_items, split_pool +from source.item.BiasedFill import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback def get_dungeon_item_pool(world): @@ -107,18 +107,17 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No if spot_to_fill: break if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - unplaced_items.insert(0, item_to_place) - if world.can_beat_game(): - if world.accessibility[item_to_place.player] != 'none': - logging.getLogger('').warning('Not all items placed. Game beatable anyway.' - f' (Could not place {item_to_place})') - continue - if world.algorithm in ['balanced', 'equitable']: - spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, - base_state, itempool, keys_in_itempool, - single_player_placement) + spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, perform_access_check, item_locations, + keys_in_itempool, single_player_placement) if spot_to_fill is None: + # we filled all reachable spots. Maybe the game can be beaten anyway? + unplaced_items.insert(0, item_to_place) + if world.can_beat_game(): + if world.accessibility[item_to_place.player] != 'none': + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {item_to_place})') + continue raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) @@ -214,6 +213,55 @@ def is_dungeon_item(item, world): or (item.map and not world.mapshuffle[item.player])) +def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, + keys_in_itempool=None, single_player_placement=False): + if world.algorithm in ['balanced', 'equitable']: + return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, + single_player_placement) + elif world.algorithm == 'vanilla_bias': + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + else: + i, config = 0, world.item_pool_config + tried = set(attempted) + if not item_to_place.is_inside_dungeon_item(world): + while i < len(config.location_groups[item_to_place.player]): + fallback_locations = config.location_groups[item_to_place.player][i].locations + other_locs = [x for x in locations if x.name in fallback_locations] + for location in other_locs: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + i += 1 + tried.update(other_locs) + else: + other_locations = vanilla_fallback(item_to_place, locations, world) + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + tried.update(other_locations) + other_locations = [x for x in locations if x not in tried] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + else: + other_locations = [x for x in locations if x not in attempted] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + + def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -232,7 +280,12 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite possible_swaps = [x for x in state.locations_checked if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) + return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + +def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): for location in swap_locations: old_item = location.item new_pool = list(itempool) + [old_item] @@ -301,6 +354,9 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_bias'] + # dungeon bias may fill up the dungeon... and push items out into the overworld + # fill in gtower locations with trash first for player in range(1, world.players + 1): if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed' or world.logic[player] in ['owglitches', 'nologic']: @@ -325,40 +381,36 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # todo: crossed progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)} - if world.algorithm in ['balanced', 'equitable', 'vanilla_bias', 'dungeon_bias', 'entangled']: - fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + + # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia + progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + random.shuffle(fill_locations) + if world.algorithm == 'balanced': + fast_fill(world, prioitempool, fill_locations) + elif world.algorithm == 'vanilla_bias': + fast_vanilla_fill(world, prioitempool, fill_locations) + elif world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias', 'entangled']: + filtered_fill(world, prioitempool, fill_locations) + else: # just need to ensure dungeon items still get placed in dungeons + fast_equitable_fill(world, prioitempool, fill_locations) + # placeholder work + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': random.shuffle(fill_locations) - if world.algorithm == 'balanced': - fast_fill(world, prioitempool, fill_locations) - elif world.algorithm == 'vanilla_bias': - fast_vanilla_fill(world, prioitempool, fill_locations) - elif world.algorithm in ['dungeon_bias', 'entangled']: - filtered_fill(world, prioitempool, fill_locations) - else: # just need to ensure dungeon items still get placed in dungeons - fast_equitable_fill(world, prioitempool, fill_locations) - # placeholder work - if world.algorithm == 'entangled' and world.players > 1: - random.shuffle(fill_locations) + placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_items = len(placeholder_items) + if num_ph_items > 0: placeholder_locations = filter_locations('Placeholder', fill_locations, world) - placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_locations = len(placeholder_locations) + if num_ph_items < num_ph_locations < len(fill_locations): + for _ in range(num_ph_locations - num_ph_items): + placeholder_items.append(replace_trash_item(restitempool, 'Rupee (1)')) + assert len(placeholder_items) == len(placeholder_locations) for i in placeholder_items: restitempool.remove(i) for l in placeholder_locations: fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) - else: - primary, secondary = split_pool(progitempool, world) - fill_restrictive(world, world.state, fill_locations, primary, keys_in_pool, False, secondary) - random.shuffle(fill_locations) - tertiary, quaternary = split_pool(prioitempool, world) - prioitempool = [] - filtered_equitable_fill(world, tertiary, fill_locations) - prioitempool += tertiary - random.shuffle(fill_locations) - fill_restrictive(world, world.state, fill_locations, secondary, keys_in_pool) - random.shuffle(fill_locations) - fast_equitable_fill(world, quaternary, fill_locations) - prioitempool += quaternary if world.algorithm == 'vanilla_bias': fast_vanilla_fill(world, restitempool, fill_locations) @@ -389,6 +441,7 @@ def filtered_fill(world, item_pool, fill_locations): # sweep once to pick up preplaced items world.state.sweep_for_events() + def fast_vanilla_fill(world, item_pool, fill_locations): while item_pool and fill_locations: item_to_place = item_pool.pop() diff --git a/Main.py b/Main.py index 455cbe41..29e4fe57 100644 --- a/Main.py +++ b/Main.py @@ -207,12 +207,10 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) - if args.algorithm in ['balanced', 'dungeon_bias', 'entangled']: + if args.algorithm != 'equitable': shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) - elif args.algorithm == 'equitable': - promote_dungeon_items(world) else: promote_dungeon_items(world) diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 45301093..47aab289 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -1,5 +1,6 @@ import RaceRandom as random import logging +from math import ceil from collections import defaultdict from DoorShuffle import validate_vanilla_reservation @@ -29,7 +30,7 @@ class LocationGroup(object): self.retro = False def locs(self, locs): - self.locations = locs + self.locations = list(locs) return self def flags(self, k, d=False, s=False, r=False): @@ -52,9 +53,10 @@ def create_item_pool_config(world): d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') for dungeon in world.dungeons: - for item in dungeon.all_items: - if item.map or item.compass: - item.advancement = True + if world.restrict_boss_items[dungeon.player] != 'none': + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True if world.algorithm == 'vanilla_bias': config.static_placement = {} config.location_groups = {} @@ -70,9 +72,12 @@ def create_item_pool_config(world): # todo: retro (universal keys...) # retro + shops config.location_groups[player] = [ + LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']), LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), 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) elif world.algorithm == 'major_bias': config.location_groups = [ LocationGroup('MajorItems'), @@ -102,6 +107,7 @@ def create_item_pool_config(world): pass # todo: 5 locations for single arrow representation? config.item_pool[player] = determine_major_items(world, player) config.location_groups[0].locations = set(groups.locations) + config.reserved_locations[player].add(groups.locations) backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) @@ -121,6 +127,36 @@ def create_item_pool_config(world): backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major'] + mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) + elif world.algorithm == 'cluster_bias': + config.location_groups = [ + LocationGroup('Clusters'), + ] + item_cnt = defaultdict(int) + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt[player] += count_major_items(world, player) + # set cluster choices + cluster_choices = figure_out_clustered_choices(world) + + chosen_locations = defaultdict(set) + placeholder_cnt = 0 + for player in range(1, world.players + 1): + number_of_clusters = ceil(item_cnt[player] / 13) + location_cnt = 0 + while item_cnt[player] > location_cnt: + chosen_clusters = random.sample(cluster_choices[player], number_of_clusters) + for loc_group in chosen_clusters: + for location in loc_group.locations: + if not location_prefilled(location, world, player): + world.item_pool_config.reserved_locations[player].add(location) + chosen_locations[location].add(player) + location_cnt += 1 + cluster_choices[player] = [x for x in cluster_choices[player] if x not in chosen_clusters] + number_of_clusters = 1 + placeholder_cnt += location_cnt - item_cnt[player] + config.placeholders = placeholder_cnt + config.location_groups[0].locations = chosen_locations elif world.algorithm == 'entangled' and world.players > 1: config.location_groups = [ LocationGroup('Entangled'), @@ -169,6 +205,14 @@ def create_item_pool_config(world): config.location_groups[0].locations = chosen_locations +def location_prefilled(location, world, player): + if world.swords[player] == 'vanilla': + return location in vanilla_swords + if world.goal[player] == 'pedestal': + return location == 'Master Sword Pedestal' + return False + + def previously_reserved(location, world, player): if '- Boss' in location.name: if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] @@ -188,7 +232,7 @@ def massage_item_pool(world): player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: - if item not in player_pool[item.player]: # filters out maps, compasses, etc + if (not item.compass and not item.map) or item not in player_pool[item.player]: player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: @@ -220,6 +264,9 @@ def massage_item_pool(world): deleted = trash_options.pop() world.itempool.remove(deleted) removed += 1 + if world.item_pool_config.placeholders > len(single_rupees): + for _ in range(world.item_pool_config.placeholders-len(single_rupees)): + single_rupees.append(ItemFactory('Rupee (1)', random.randint(1, world.players))) placeholders = random.sample(single_rupees, world.item_pool_config.placeholders) world.itempool += placeholders removed -= len(placeholders) @@ -227,6 +274,19 @@ def massage_item_pool(world): world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players))) +def replace_trash_item(item_pool, replacement): + trash_options = [x for x in item_pool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + item_pool.remove(deleted) + replace_item = ItemFactory(replacement, deleted.player) + item_pool.append(replace_item) + return replace_item + + def validate_reservation(location, dungeon, world, player): world.item_pool_config.reserved_locations[player].add(location.name) if world.doorShuffle[player] != 'vanilla': @@ -265,12 +325,22 @@ def count_major_items(world, player): major_item_set += world.triforce_pool[player] if world.bombbag[player]: major_item_set += world.triforce_pool[player] - # todo: vanilla, assured, swordless? - # if world.swords[player] != "random": - # if world.swords[player] == 'assured': - # major_item_set -= 1 - # if world.swords[player] in ['vanilla', 'swordless']: - # major_item_set -= 4 + if world.swords[player] != "random": + if world.swords[player] == 'assured': + major_item_set -= 1 + if world.swords[player] in ['vanilla', 'swordless']: + major_item_set -= 4 + if world.retro[player]: + if world.shopsanity[player]: + major_item_set -= 1 # sword in old man cave + if world.keyshuffle[player]: + major_item_set -= 29 + # universal keys + major_item_set += 19 if world.difficulty[player] == 'normal' else 14 + if world.mode[player] == 'standard' and world.doorShuffle[player] == 'vanilla': + major_item_set -= 1 # a key in escape + if world.doorShuffle[player] != 'vanilla': + major_item_set += 10 # tries to add up to 10 more universal keys for door rando # todo: starting equipment? return major_item_set @@ -354,16 +424,68 @@ def classify_major_items(world): item.priority = True -def split_pool(pool, world): - # bias or entangled - config = world.item_pool_config - priority, secondary = [], [] - for item in pool: - if item.name in config.item_pool[item.player]: - priority.append(item) - else: - secondary.append(item) - return priority, secondary +def figure_out_clustered_choices(world): + cluster_candidates = {} + for player in range(1, world.players + 1): + cluster_candidates[player] = [LocationGroup(x.name).locs(x.locations) for x in clustered_groups] + backups = list(reversed(leftovers)) + if world.bigkeyshuffle[player]: + bk_grp = LocationGroup('BigKeys').locs(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + bk_grp.locations.append(mode_grouping['Big Key Drops']) + for i in range(13-len(bk_grp.locations)): + bk_grp.locations.append(backups.pop()) + cluster_candidates[player].append(bk_grp) + if world.compassshuffle[player]: + cmp_grp = LocationGroup('Compasses').locs(mode_grouping['Compasses']) + if len(cmp_grp.locations) + len(backups) >= 13: + for i in range(13-len(cmp_grp.locations)): + cmp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(cmp_grp) + else: + backups.extend(reversed(cmp_grp.locations)) + if world.mapshuffle[player]: + mp_grp = LocationGroup('Maps').locs(mode_grouping['Maps']) + if len(mp_grp.locations) + len(backups) >= 13: + for i in range(13-len(mp_grp.locations)): + mp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(mp_grp) + else: + backups.extend(reversed(mp_grp.locations)) + if world.shopsanity[player]: + cluster_candidates[player].append(LocationGroup('Shopsanity1').locs(other_clusters['Shopsanity1'])) + cluster_candidates[player].append(LocationGroup('Shopsanity2').locs(other_clusters['Shopsanity2'])) + extras = list(other_clusters['ShopsanityLeft']) + if world.retro[player]: + extras.extend(mode_grouping['RetroShops']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('ShopExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + if world.keyshuffle[player] or world.retro[player]: + cluster_candidates[player].append(LocationGroup('SmallKey1').locs(other_clusters['SmallKey1'])) + cluster_candidates[player].append(LocationGroup('SmallKey2').locs(other_clusters['SmallKey2'])) + extras = list(other_clusters['SmallKeyLeft']) + if world.keydropshuffle[player]: + cluster_candidates[player].append(LocationGroup('KeyDrop1').locs(other_clusters['KeyDrop1'])) + cluster_candidates[player].append(LocationGroup('KeyDrop2').locs(other_clusters['KeyDrop2'])) + extras.extend(other_clusters['KeyDropLeft']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('SmallKeyExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + return cluster_candidates + + +def vanilla_fallback(item_to_place, locations, world): + if item_to_place.is_inside_dungeon_item(world): + return [x for x in locations if x.name in vanilla_fallback_dungeon_set + and x.parent_region.dungeon and x.parent_region.dungeon.name == item_to_place.dungeon] + return [] def filter_locations(item_to_place, locations, world): @@ -391,7 +513,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.name in restricted] # bias toward certain location in overflow? (thinking about this for major_bias) return filtered if len(filtered) > 0 else locations - if world.algorithm == 'entangled' and world.players > 1: + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': config = world.item_pool_config if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -417,7 +539,7 @@ vanilla_mapping = { 'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'], 'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'], 'Bow': ['Eastern Palace - Big Chest'], - 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Left'], + 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Right'], 'Book of Mudora': ['Library'], 'Hammer': ['Palace of Darkness - Big Chest'], 'Hookshot': ['Swamp Palace - Big Chest'], @@ -443,10 +565,10 @@ vanilla_mapping = { 'Master Sword': ['Master Sword Pedestal'], 'Tempered Sword': ['Blacksmith'], 'Fighter Sword': ["Link's Uncle"], - 'Golden Sword': ['Pyramid Fairy - Right'], - 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Right'], + 'Golden Sword': ['Pyramid Fairy - Left'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Left'], 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], - 'Silver Arrows': ['Pyramid Fairy - Left'], + 'Silver Arrows': ['Pyramid Fairy - Right'], 'Single Arrow': ['Palace of Darkness - Dark Basement - Left'], 'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right', 'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left', @@ -702,6 +824,11 @@ mode_grouping = { ] } +vanilla_fallback_dungeon_set = set(mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', @@ -714,148 +841,122 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod 'Progressive Bow', 'Progressive Bow (Alt)'} -# todo: re-enter these clustered_groups = [ LocationGroup("MajorRoute1").locs([ - 'Library', 'Master Sword Pedestal', 'Old Man', 'Flute Spot', - 'Ether Tablet', 'Stumpy', 'Bombos Tablet', 'Mushroom', 'Bottle Merchant', 'Kakariko Tavern', - 'Sick Kid', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right' + 'Ice Rod Cave', 'Library', 'Old Man', 'Magic Bat', 'Ether Tablet', 'Hobo', 'Purple Chest', 'Spike Cave', + 'Sahasrahla', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right' ]), LocationGroup("MajorRoute2").locs([ - 'King Zora', 'Sahasrahla', 'Ice Rod Cave', 'Catfish', - 'Purple Chest', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', - 'Magic Bat', 'Hobo', 'Potion Shop', 'Spike Cave', "King's Tomb" + 'Mushroom', 'Secret Passage', 'Bottle Merchant', 'Flute Spot', 'Catfish', 'Stumpy', 'Waterfall Fairy - Left', + 'Waterfall Fairy - Right', 'Master Sword Pedestal', "Thieves' Town - Attic", 'Sewers - Secret Room - Right', + 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle' ]), - LocationGroup("BigChest").locs([ - 'Sanctuary', 'Eastern Palace - Big Chest', - 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', 'Palace of Darkness - Big Chest', - 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', "Thieves' Town - Big Chest", - 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', 'Ice Palace - Big Chest', - 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest' + LocationGroup("MajorRoute3").locs([ + 'Kakariko Tavern', 'Sick Kid', 'King Zora', 'Potion Shop', 'Bombos Tablet', "King's Tomb", 'Blacksmith', + 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left' ]), - LocationGroup("BossUncle").locs([ - "Link's Uncle", "Link's House", 'Secret Passage', 'Eastern Palace - Boss', - 'Desert Palace - Boss', 'Tower of Hera - Boss', 'Palace of Darkness - Boss', 'Swamp Palace - Boss', - 'Skull Woods - Boss', "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', - 'Turtle Rock - Boss']), - LocationGroup("HeartPieces LW").locs([ - 'Lost Woods Hideout', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Maze Race', 'Sunken Treasure', - 'Bonk Rock Cave', 'Desert Ledge', "Aginah's Cave", 'Spectacle Rock Cave', 'Spectacle Rock', 'Pyramid', - 'Lumberjack Tree', "Zora's Ledge"]), - LocationGroup("HeartPieces DW").locs([ - 'Lake Hylia Island', 'Chest Game', 'Digging Game', 'Graveyard Cave', 'Mimic Cave', - 'Cave 45', 'Peg Cave', 'Bumper Cave Ledge', 'Checkerboard Cave', 'Mire Shed - Right', 'Floating Island', - 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right']), - LocationGroup("Minor Trash").locs([ - 'Ice Palace - Freezor Chest', 'Skull Woods - Pot Prison', 'Misery Mire - Bridge Chest', - 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Maze - Top', - 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', - 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', - 'Swamp Palace - Waterfall Room', 'Turtle Rock - Eye Bridge - Bottom Right', - 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right']), - LocationGroup("CompassTT").locs([ - "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", - 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', - 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', - "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', - 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left']), - LocationGroup("Early SKs").locs([ - 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', - 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', - 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Harmless Hellway', - "Thieves' Town - Blind's Cell", 'Eastern Palace - Cannonball Chest', - 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', - 'Sewers - Secret Room - Middle', 'Floodgate Chest' - ]), - LocationGroup("Late SKs").locs([ - 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', "Hyrule Castle - Zelda's Chest", - 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Swamp Palace - West Chest', - 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', - 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', - 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest', - ]), - LocationGroup("Kak-LDM").locs([ + LocationGroup("Dungeon Major").locs([ + 'Eastern Palace - Big Chest', 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest', "Link's Uncle"]), + LocationGroup("Dungeon Heart").locs([ + 'Sanctuary', 'Eastern Palace - Boss', 'Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', "Thieves' Town - Boss", + 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss', "Link's House", + 'Ganons Tower - Validation Chest']), + LocationGroup("HeartPieces1").locs([ + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Maze Race', 'Lumberjack Tree', 'Bonk Rock Cave', 'Graveyard Cave', + 'Checkerboard Cave', "Zora's Ledge", 'Digging Game', 'Desert Ledge', 'Bumper Cave Ledge', 'Floating Island', + 'Swamp Palace - Waterfall Room']), + LocationGroup("HeartPieces2").locs([ + "Blind's Hideout - Top", 'Sunken Treasure', "Aginah's Cave", 'Mimic Cave', 'Spectacle Rock Cave', 'Cave 45', + 'Spectacle Rock', 'Lake Hylia Island', 'Chest Game', 'Mire Shed - Right', 'Pyramid', 'Peg Cave', + 'Eastern Palace - Cannonball Chest']), + LocationGroup("BlindHope").locs([ "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", - "Blind's Hideout - Far Right", 'Chicken House', 'Paradox Cave Lower - Far Left', - 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', - 'Paradox Cave Lower - Middle', 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', + "Blind's Hideout - Far Right", 'Floodgate Chest', 'Spiral Cave', 'Palace of Darkness - Dark Maze - Bottom', + 'Palace of Darkness - Dark Maze - Top', 'Swamp Palace - Flooded Room - Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right']), + LocationGroup('WellHype').locs([ + 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', ]), - LocationGroup("BK-Bunny").locs([ - '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', - 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', - 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', - 'Superbunny Cave - Top', 'Superbunny Cave - Bottom', + LocationGroup('MiniMoldormLasers').locs([ + 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', + 'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Chicken House', 'Brewery', + 'Palace of Darkness - Dark Basement - Left', 'Ice Palace - Freezor Chest', 'Swamp Palace - West Chest', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', ]), - LocationGroup("Early Drops").flags(True, True).locs([ + LocationGroup('ParadoxCloset').locs([ + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', "Hyrule Castle - Zelda's Chest", + 'C-Shaped House', 'Mire Shed - Left', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', + ]) +] + +other_clusters = { + 'SmallKey1': [ + 'Sewers - Dark Cross', 'Tower of Hera - Basement Cage', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - Dark Basement - Right', "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', + 'Misery Mire - Spike Chest', "Ganons Tower - Bob's Torch"], + 'SmallKey2': [ + 'Desert Palace - Torch', 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Harmless Hellway', 'Swamp Palace - Entrance', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Ice Palace - Spike Room', + 'Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left'], + 'SmallKeyLeft': [ + 'Ganons Tower - Tile Room', 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'KeyDrop1': [ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', - 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', - 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', - 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Hyrule Castle - Key Rat Key Drop', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop', + 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'KeyDrop2': [ + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', + 'Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', + 'Desert Palace - Desert Tiles 2 Pot Key', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + "Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Hammer Block Key Drop', + 'Ice Palace - Many Pots Pot Key', 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'KeyDropLeft': [ 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', - 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Hyrule Castle - Big Key Drop', - ]), - LocationGroup("Late Drops").flags(True, True).locs([ - 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', - 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', - 'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', - 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Ganons Tower - Conveyor Cross Pot Key', - 'Ganons Tower - Double Switch Pot Key']), - LocationGroup("SS-Hype-Voo").locs([ - 'Mini Moldorm Cave - Left', - 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', - 'Mini Moldorm Cave - Far Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', - 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', 'Brewery', - 'C-Shaped House', 'Palace of Darkness - The Arena - Ledge', - ]), - LocationGroup("DDM Hard").flags(True, True).locs([ - 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', - 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', - 'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', - 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', - 'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Roller Room - Right', - 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop' - ]), - LocationGroup("Kak Shop").flags(False, False, True).locs([ - 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', + 'Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], + 'Shopsanity1': [ 'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', + 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', - 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', - 'Capacity Upgrade - Left']), - LocationGroup("Hylia Shop").flags(False, False, True).locs([ + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', 'Capacity Upgrade - Left'], + 'Shopsanity2': [ 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', - 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', - 'Capacity Upgrade - Right']), - LocationGroup("Map Validation").locs([ - 'Hyrule Castle - Map Chest', - 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', 'Tower of Hera - Map Chest', - 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', 'Skull Woods - Map Chest', - "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', - 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest', 'Ganons Tower - Validation Chest']), - LocationGroup("SahasWell+MireHopeDDMShop").flags(False, False, True).locs([ - 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', - 'Kakariko Well - Bottom', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', - "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", - 'Mire Shed - Left', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right']), - LocationGroup("Tower Pain").flags(True).locs([ - 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', - 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', - 'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', - 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', - 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', - "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Room - Left']), - LocationGroup("Retro Shops").flags(False, False, True, True).locs([ - 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', - 'Take-Any #2 Item 1', 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2', - 'Take-Any #4 Item 1', 'Take-Any #4 Item 2', 'Swamp Palace - Entrance', - 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Top Right', - 'Ganons Tower - Compass Room - Bottom Right', - ]) + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Right', + ], + 'ShopsanityLeft': ['Potion Shop - Left', 'Potion Shop - Middle', 'Potion Shop - Right'] +} +leftovers = [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right',"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right', ] +vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyramid Fairy - Left'} trash_items = { 'Nothing': -1, From a9b872b88d32cde9670a1d579b9da52d10d2add2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:00:55 -0600 Subject: [PATCH 013/293] Ambrosia logic fixes --- DungeonGenerator.py | 2 +- Fill.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 20352b37..cb834877 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -680,7 +680,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] diff --git a/Fill.py b/Fill.py index 447e6912..59a98cea 100644 --- a/Fill.py +++ b/Fill.py @@ -254,7 +254,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return item.is_inside_dungeon_item(world) + return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this def valid_reserved_placement(item, location, world): From c64d499bab652d847990529e0f41cd5aad22dba4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:03:51 -0600 Subject: [PATCH 014/293] Maps/compasses should not be advancement items if the restriction is none --- source/item/FillUtil.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 6aa2f5cf..0a1e54f7 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -20,6 +20,7 @@ def create_item_pool_config(world): d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') for dungeon in world.dungeons: - for item in dungeon.all_items: - if item.map or item.compass: - item.advancement = True + if world.restrict_boss_items[dungeon.player] != 'none': + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True From 058b78cff938c905afe7be165da1b8e20603c4bd Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 23 Sep 2021 16:10:07 -0600 Subject: [PATCH 015/293] Massage fix --- source/item/BiasedFill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 47aab289..2eeb8a6b 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -232,7 +232,7 @@ def massage_item_pool(world): player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: - if (not item.compass and not item.map) or item not in player_pool[item.player]: + if item.is_inside_dungeon_item(world): player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: From 28b87428cc79de844a72aaf0f53c1f48b1484be8 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 5 Oct 2021 13:58:30 -0600 Subject: [PATCH 016/293] Changed bias named. Added districting --- Fill.py | 43 +++-- Main.py | 3 +- RaceRandom.py | 1 + Rom.py | 40 ++++- resources/app/cli/args.json | 9 +- resources/app/cli/lang/en.json | 17 +- resources/app/gui/lang/en.json | 8 +- resources/app/gui/randomize/item/widgets.json | 8 +- source/item/BiasedFill.py | 99 ++++++++-- source/item/District.py | 169 ++++++++++++++++++ 10 files changed, 341 insertions(+), 56 deletions(-) create mode 100644 source/item/District.py diff --git a/Fill.py b/Fill.py index 872f09e7..a770ee71 100644 --- a/Fill.py +++ b/Fill.py @@ -27,7 +27,7 @@ def promote_dungeon_items(world): def dungeon_tracking(world): for dungeon in world.dungeons: layout = world.dungeon_layouts[dungeon.player][dungeon.name] - layout.dungeon_items = len(dungeon.all_items) + layout.dungeon_items = len([i for i in dungeon.all_items if i.is_inside_dungeon_item(world)]) layout.free_items = layout.location_cnt - layout.dungeon_items @@ -64,13 +64,10 @@ def fill_dungeons_restrictive(world, shuffled_locations): def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, - reserved_items=None): - if not reserved_items: - reserved_items = [] - + vanilla=False): def sweep_from_pool(): new_state = base_state.copy() - for item in itempool + reserved_items: + for item in itempool: new_state.collect(item, True) new_state.sweep_for_events() return new_state @@ -99,7 +96,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No spot_to_fill = None - item_locations = filter_locations(item_to_place, locations, world) + item_locations = filter_locations(item_to_place, locations, world, vanilla) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, single_player_placement, perform_access_check, itempool, @@ -107,6 +104,9 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No if spot_to_fill: break if spot_to_fill is None: + if vanilla: + unplaced_items.insert(0, item_to_place) + continue spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, base_state, itempool, perform_access_check, item_locations, keys_in_itempool, single_player_placement) @@ -166,7 +166,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this + return not item.is_inside_dungeon_item(world) def valid_reserved_placement(item, location, world): @@ -215,10 +215,11 @@ def is_dungeon_item(item, world): def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, keys_in_itempool=None, single_player_placement=False): + logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery') if world.algorithm in ['balanced', 'equitable']: return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, single_player_placement) - elif world.algorithm == 'vanilla_bias': + 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'] return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, @@ -354,8 +355,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] - gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_bias'] - # dungeon bias may fill up the dungeon... and push items out into the overworld + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_only'] + # dungeon only may fill up the dungeon... and push items out into the overworld # fill in gtower locations with trash first for player in range(1, world.players + 1): @@ -384,18 +385,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + if world.algorithm == 'vanilla_fill': + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool, vanilla=True) fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) random.shuffle(fill_locations) if world.algorithm == 'balanced': fast_fill(world, prioitempool, fill_locations) - elif world.algorithm == 'vanilla_bias': + elif world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, prioitempool, fill_locations) - elif world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias', 'entangled']: + elif world.algorithm in ['major_only', 'dungeon_only', 'district']: filtered_fill(world, prioitempool, fill_locations) else: # just need to ensure dungeon items still get placed in dungeons fast_equitable_fill(world, prioitempool, fill_locations) # placeholder work - if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': + if world.algorithm == 'district': random.shuffle(fill_locations) placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] num_ph_items = len(placeholder_items) @@ -412,7 +415,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) - if world.algorithm == 'vanilla_bias': + if world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, restitempool, fill_locations) else: fast_fill(world, restitempool, fill_locations) @@ -443,8 +446,18 @@ def filtered_fill(world, item_pool, fill_locations): def fast_vanilla_fill(world, item_pool, fill_locations): + next_item_pool = [] while item_pool and fill_locations: item_to_place = item_pool.pop() + locations = filter_locations(item_to_place, fill_locations, world, vanilla_skip=True) + if len(locations): + spot_to_fill = locations.pop() + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + else: + next_item_pool.append(item_to_place) + while next_item_pool and fill_locations: + item_to_place = next_item_pool.pop() spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world))) fill_locations.remove(spot_to_fill) world.push_item(spot_to_fill, item_to_place, False) diff --git a/Main.py b/Main.py index ca146f5d..09ebb7e5 100644 --- a/Main.py +++ b/Main.py @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -from source.item.BiasedFill import create_item_pool_config, massage_item_pool +from source.item.BiasedFill import create_item_pool_config, massage_item_pool, district_item_pool_config __version__ = '1.0.0.1-u' @@ -199,6 +199,7 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) + district_item_pool_config(world) massage_item_pool(world) logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) diff --git a/RaceRandom.py b/RaceRandom.py index 127d966d..fa882580 100644 --- a/RaceRandom.py +++ b/RaceRandom.py @@ -22,6 +22,7 @@ def _wrap(name): # These are for intellisense purposes only, and will be overwritten below choice = _prng_inst.choice +choices = _prng_inst.choices gauss = _prng_inst.gauss getrandbits = _prng_inst.getrandbits randint = _prng_inst.randint diff --git a/Rom.py b/Rom.py index 22f017e3..a7ab66ca 100644 --- a/Rom.py +++ b/Rom.py @@ -17,7 +17,8 @@ except ImportError: from BaseClasses import CollectionState, ShopType, Region, Location, Door, DoorType, RegionType, PotItem from DoorShuffle import compass_data, DROptions, boss_indicator -from Dungeons import dungeon_music_addresses +from Dungeons import dungeon_music_addresses, dungeon_table +from DungeonGenerator import dungeon_portals from Regions import location_table, shop_to_location_table, retro_shops from RoomData import DoorKind from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable @@ -32,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '11f4f494e999a919aafd7d2624e67679' +RANDOMIZERBASEHASH = 'f2791b1fb0776849bd4a0851b75fca26' class JsonRom(object): @@ -2051,6 +2052,7 @@ def write_strings(rom, world, player, team): else: entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0 + hint_count -= 2 if world.algorithm == 'district' and world.shuffle[player] not in ['simple', 'restricted'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: if hint_count > 0: @@ -2142,11 +2144,35 @@ def write_strings(rom, world, player, team): else: tt[hint_locations.pop(0)] = this_hint - # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint isn't selected twice. - junk_hints = junk_texts.copy() - random.shuffle(junk_hints) - for location in hint_locations: - tt[location] = junk_hints.pop(0) + if world.shuffle[player] in ['full', 'crossed', 'insanity']: + # 3 hints for dungeons - todo: replace with overworld map code + hint_count = 3 + dungeon_candidates = list(dungeon_table.keys()) + dungeon_choices = random.choices(dungeon_candidates, k=hint_count) + for c in dungeon_choices: + portal_name = random.choice(dungeon_portals[c]) + portal_region = world.get_portal(portal_name, player).door.entrance.connected_region + entrance = next(ent for ent in portal_region.entrances + if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + district =next(d for d in world.districts[player].values() if entrance.name in d.entrances) + this_hint = f'The entrance to {c} can be found in {district.name}' + tt[hint_locations.pop(0)] = this_hint + + if world.algorithm == 'district': + hint_candidates = [] + for name, district in world.districts[player].items(): + if name not in world.item_pool_config.recorded_choices and not district.sphere_one: + hint_candidates.append(f'{name} is a foolish choice') + random.shuffle(hint_candidates) + for location in hint_locations: + tt[location] = hint_candidates.pop(0) + else: + # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint + # isn't selected twice. + junk_hints = junk_texts.copy() + random.shuffle(junk_hints) + for location in hint_locations: + tt[location] = junk_hints.pop(0) # We still need the older hints of course. Those are done here. diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 99ddec22..3a316a69 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -102,11 +102,10 @@ "choices": [ "balanced", "equitable", - "vanilla_bias", - "major_bias", - "dungeon_bias", - "cluster_bias", - "entangled" + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 7b7bb4f7..fe536d75 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -156,27 +156,26 @@ " algorithm.", "equitable: does not place dungeon items first allowing new potential", " but mixed with the normal advancement pool", - "biased placements: these consider all major items to be special and attempts", + "restricted placements: these consider all major items to be special and attempts", "to place items from fixed to semi-random locations. For purposes of these shuffles, all", "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool", "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),", "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and", "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.", - "vanilla_bias Same as above, but attempts to place items in their vanilla", + "vanilla_fill As above, but attempts to place items in their vanilla", " location first. Major items that cannot be placed that way", " will attempt to be placed in other failed locations first.", - " Also attempts to place junk items in vanilla locations", - "major_bias same as above, but uses the major items' location preferentially", + " Also attempts to place all items in vanilla locations", + "major_only As above, but uses the major items' location preferentially", " major item location are defined as the group of location where", - " the items are found in the vanilla game. Backup locations for items", - " not in the vanilla game will be in the documentation", - "dungeon_bias same as above, but major items are preferentially placed", + " the items are found in the vanilla game.", + "dungeon_only As above, but major items are preferentially placed", " in dungeons locations first", - "cluster_bias same as above, but groups of locations are chosen randomly", + "district As above, but groups of locations are chosen randomly", " from a pool of fixed locations designed to be interesting", " and give major clues about the location of other", - " advancement items. These fixed groups will be documented" + " advancement items. These fixed groups will be documented." ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 4cfeaafb..a2aeb0a2 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -281,10 +281,10 @@ "randomizer.item.sortingalgo": "Item Sorting", "randomizer.item.sortingalgo.balanced": "Balanced", "randomizer.item.sortingalgo.equitable": "Equitable", - "randomizer.item.sortingalgo.vanilla_bias": "Biased: Vanilla", - "randomizer.item.sortingalgo.major_bias": "Biased: Major Items", - "randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons", - "randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered", + "randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill", + "randomizer.item.sortingalgo.major_only": "Major Location Restriction", + "randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction", + "randomizer.item.sortingalgo.district": "District Restriction", "randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items.none": "None", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 7f524a33..7b76d8fe 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -118,10 +118,10 @@ "options": [ "balanced", "equitable", - "vanilla_bias", - "major_bias", - "dungeon_bias", - "cluster_bias" + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "restrict_boss_items": { diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 2eeb8a6b..96e33cfd 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -3,6 +3,7 @@ import logging from math import ceil from collections import defaultdict +from source.item.District import resolve_districts from DoorShuffle import validate_vanilla_reservation from Dungeons import dungeon_table from Items import item_table, ItemFactory @@ -17,6 +18,8 @@ class ItemPoolConfig(object): self.placeholders = None self.reserved_locations = defaultdict(set) + self.recorded_choices = [] + class LocationGroup(object): def __init__(self, name): @@ -57,7 +60,7 @@ def create_item_pool_config(world): for item in dungeon.all_items: if item.map or item.compass: item.advancement = True - if world.algorithm == 'vanilla_bias': + if world.algorithm == 'vanilla_fill': config.static_placement = {} config.location_groups = {} for player in range(1, world.players + 1): @@ -78,7 +81,7 @@ 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) - elif world.algorithm == 'major_bias': + elif world.algorithm == 'major_only': config.location_groups = [ LocationGroup('MajorItems'), LocationGroup('Backup') @@ -111,7 +114,7 @@ def create_item_pool_config(world): backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) - elif world.algorithm == 'dungeon_bias': + elif world.algorithm == 'dungeon_only': config.location_groups = [ LocationGroup('Dungeons'), LocationGroup('Backup') @@ -205,6 +208,74 @@ def create_item_pool_config(world): config.location_groups[0].locations = chosen_locations +def district_item_pool_config(world): + resolve_districts(world) + if world.algorithm == 'district': + config = world.item_pool_config + config.location_groups = [ + LocationGroup('Districts'), + ] + item_cnt = 0 + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + # set district choices + district_choices = {} + for p in range(1, world.players + 1): + for name, district in world.districts[p].items(): + adjustment = 0 + if district.dungeon: + adjustment = len([i for i in world.get_dungeon(name, p).all_items + if i.is_inside_dungeon_item(world)]) + dist_len = len(district.locations) - adjustment + if name not in district_choices: + district_choices[name] = (district.sphere_one, dist_len) + else: + so, amt = district_choices[name] + district_choices[name] = (so or district.sphere_one, amt + dist_len) + + chosen_locations = defaultdict(set) + location_cnt = 0 + + # choose a sphere one district + sphere_one_choices = [d for d, info in district_choices.items() if info[0]] + sphere_one = random.choice(sphere_one_choices) + so, amt = district_choices[sphere_one] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][sphere_one].locations: + chosen_locations[location].add(player) + del district_choices[sphere_one] + config.recorded_choices.append(sphere_one) + + scale_factors = defaultdict(int) + scale_total = 0 + for p in range(1, world.players + 1): + ent = 'Inverted Ganons Tower' if world.mode[p] == 'inverted' else 'Ganons Tower' + dungeon = world.get_entrance(ent, p).connected_region.dungeon + if dungeon: + scale = world.crystals_needed_for_gt[p] + scale_total += scale + scale_factors[dungeon.name] += scale + scale_total = max(1, scale_total) + scale_divisors = defaultdict(lambda: 1) + scale_divisors.update(scale_factors) + + while location_cnt < item_cnt: + weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] + choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] + so, amt = district_choices[choice] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][choice].locations: + chosen_locations[location].add(player) + del district_choices[choice] + config.recorded_choices.append(choice) + config.placeholders = location_cnt - item_cnt + config.location_groups[0].locations = chosen_locations + + def location_prefilled(location, world, player): if world.swords[player] == 'vanilla': return location in vanilla_swords @@ -390,6 +461,8 @@ def calc_dungeon_limits(world, player): def determine_major_items(world, player): major_item_set = set(major_items) + if world.progressive == 'off': + pass # now what? if world.bigkeyshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player]: @@ -412,16 +485,18 @@ def determine_major_items(world, player): def classify_major_items(world): - if world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled' - and world.players > 1): + if world.algorithm in ['major_only', 'dungeon_only', 'district']: config = world.item_pool_config for item in world.itempool: if item.name in config.item_pool[item.player]: - if not item.advancement or not item.priority: + if not item.advancement and not item.priority: if item.smallkey or item.bigkey: item.advancement = True else: item.priority = True + else: + if item.priority: + item.priority = False def figure_out_clustered_choices(world): @@ -488,13 +563,15 @@ def vanilla_fallback(item_to_place, locations, world): return [] -def filter_locations(item_to_place, locations, world): - if world.algorithm == 'vanilla_bias': +def filter_locations(item_to_place, locations, world, vanilla_skip=False): + if world.algorithm == 'vanilla_fill': config, filtered = world.item_pool_config, [] item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name if item_name in config.static_placement[item_to_place.player]: restricted = config.static_placement[item_to_place.player][item_name] filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + if vanilla_skip and len(filtered) == 0: + return filtered i = 0 while len(filtered) <= 0: if i >= len(config.location_groups[item_to_place.player]): @@ -503,7 +580,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] i += 1 return filtered - if world.algorithm in ['major_bias', 'dungeon_bias']: + if world.algorithm in ['major_only', 'dungeon_only']: config = world.item_pool_config if item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -513,7 +590,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.name in restricted] # bias toward certain location in overflow? (thinking about this for major_bias) return filtered if len(filtered) > 0 else locations - if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'district': config = world.item_pool_config if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -835,7 +912,7 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', - 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Blue Shield', 'Red Shield', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', 'Progressive Bow', 'Progressive Bow (Alt)'} diff --git a/source/item/District.py b/source/item/District.py new file mode 100644 index 00000000..7b48ede8 --- /dev/null +++ b/source/item/District.py @@ -0,0 +1,169 @@ +from collections import deque + +from BaseClasses import CollectionState, RegionType +from Dungeons import dungeon_table + + +class District(object): + + def __init__(self, name, locations, entrances=None, dungeon=None): + self.name = name + self.dungeon = dungeon + self.locations = locations + self.entrances = entrances if entrances else [] + self.sphere_one = False + + +def create_districts(world): + world.districts = {} + for p in range(1, world.players + 1): + create_district_helper(world, p) + + +def create_district_helper(world, player): + inverted = world.mode[player] == 'inverted' + districts = {} + kak_locations = {'Bottle Merchant', 'Kakariko Tavern', 'Maze Race'} + nw_lw_locations = {'Mushroom', 'Master Sword Pedestal'} + central_lw_locations = {'Sunken Treasure', 'Flute Spot'} + desert_locations = {'Purple Chest', 'Desert Ledge'} + lake_locations = {'Hobo'} + east_lw_locations = {"Zora's Ledge", 'King Zora'} + lw_dm_locations = {'Old Man', 'Spectacle Rock', 'Ether Tablet'} + east_dw_locations = {'Pyramid', 'Catfish'} + south_dw_locations = {'Stumpy', 'Digging Game', 'Bombos Tablet', 'Lake Hylia Island'} + voo_north_locations = {'Bumper Cave Ledge'} + ddm_locations = {'Floating Island'} + + kak_entrances = ['Kakariko Well Cave', 'Bat Cave Cave', 'Elder House (East)', 'Elder House (West)', + 'Two Brothers House (East)', 'Two Brothers House (West)', 'Blinds Hideout', 'Chicken House', + 'Blacksmiths Hut', 'Sick Kids House', 'Snitch Lady (East)', 'Snitch Lady (West)', + 'Bush Covered House', 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Library', + 'Kakariko Gamble Game', 'Kakariko Well Drop', 'Bat Cave Drop'] + nw_lw_entrances = ['North Fairy Cave', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', + 'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Kings Grave', 'Lost Woods Gamble', + 'Fortune Teller (Light)', 'Bonk Rock Cave', 'Lumberjack House', 'North Fairy Cave Drop', + 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] + central_lw_entrances = ['Links House', 'Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', 'Agahnims Tower', 'Hyrule Castle Secret Entrance Stairs', + 'Dam', 'Bonk Fairy (Light)', 'Light Hype Fairy', 'Cave Shop (Lake Hylia)', + 'Lake Hylia Fortune Teller', 'Hyrule Castle Secret Entrance Drop'] + desert_entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', + 'Desert Palace Entrance (North)', 'Desert Palace Entrance (East)', 'Desert Fairy', + 'Aginahs Cave', '50 Rupee Cave'] + lake_entrances = ['Capacity Upgrade', 'Mini Moldorm Cave', 'Good Bee Cave', '20 Rupee Cave', 'Ice Rod Cave'] + east_lw_entrances = ['Eastern Palace', 'Waterfall of Wishing', 'Lake Hylia Fairy', 'Sahasrahlas Hut', + 'Long Fairy Cave', 'Potion Shop'] + lw_dm_entrances = ['Tower of Hera', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave', + 'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', + 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', + 'Spiral Cave', 'Spiral Cave (Bottom)', 'Hookshot Fairy'] + east_dw_entrances = ['Palace of Darkness', 'Pyramid Entrance', 'Pyramid Fairy', 'East Dark World Hint', + 'Palace of Darkness Hint', 'Dark Lake Hylia Fairy', 'Dark World Potion Shop', 'Pyramid Hole'] + south_dw_entrances = ['Ice Palace', 'Swamp Palace', 'Dark Lake Hylia Ledge Fairy', + 'Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Hint', 'Hype Cave', + 'Bonk Fairy (Dark)', 'Archery Game', 'Big Bomb Shop', 'Dark Lake Hylia Shop', 'Cave 45'] + voo_north_entrances = ['Thieves Town', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', + 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint', + 'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop', 'Graveyard Cave', + 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + mire_entrances = ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'Checkerboard Cave'] + ddm_entrances = ['Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)', + 'Hookshot Cave', 'Hookshot Cave Back Entrance', 'Ganons Tower', 'Spike Cave', + 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave'] + + if inverted: + south_dw_locations.remove('Bombos Tablet') + south_dw_locations.remove('Lake Hylia Island') + voo_north_locations.remove('Bumper Cave Ledge') + ddm_locations.remove('Floating Island') + desert_locations.add('Bombos Tablet') + lake_locations.add('Lake Hylia Island') + nw_lw_locations.add('Bumper Cave Ledge') + lw_dm_locations.add('Floating Island') + + south_dw_entrances.remove('Cave 45') + central_lw_entrances.append('Cave 45') + voo_north_entrances.remove('Graveyard Cave') + nw_lw_entrances.append('Graveyard Cave') + mire_entrances.remove('Checkerboard Cave') + desert_entrances.append('Checkerboard Cave') + ddm_entrances.remove('Mimic Cave') + lw_dm_entrances.append('Mimic Cave') + + south_dw_entrances.remove('Big Bomb Shop') + central_lw_entrances.append('Inverted Big Bomb Shop') + central_lw_entrances.remove('Links House') + south_dw_entrances.append('Inverted Links House') + voo_north_entrances.remove('Dark Sanctuary') + voo_north_entrances.append('Inverted Dark Sanctuary') + ddm_entrances.remove('Ganons Tower') + central_lw_entrances.append('Inverted Ganons Tower') + central_lw_entrances.remove('Agahnims Tower') + ddm_entrances.append('Inverted Agahnims Tower') + east_dw_entrances.remove('Pyramid Entrance') + central_lw_entrances.append('Inverted Pyramid Entrance') + east_dw_entrances.remove('Pyramid Hole') + central_lw_entrances.append('Inverted Pyramid Hole') + + districts['Kakariko'] = District('Kakariko', kak_locations, entrances=kak_entrances) + districts['Northwest Hyrule'] = District('Northwest Hyrule', nw_lw_locations, entrances=nw_lw_entrances) + districts['Central Hyrule'] = District('Central Hyrule', central_lw_locations, entrances=central_lw_entrances) + districts['Desert'] = District('Desert', desert_locations, entrances=desert_entrances) + districts['Lake Hylia'] = District('Lake Hylia', lake_locations, entrances=lake_entrances) + districts['Eastern Hyrule'] = District('Eastern Hyrule', east_lw_locations, entrances=east_lw_entrances) + districts['Death Mountain'] = District('Death Mountain', lw_dm_locations, entrances=lw_dm_entrances) + districts['East Dark World'] = District('East Dark World', east_dw_locations, entrances=east_dw_entrances) + districts['South Dark World'] = District('South Dark World', south_dw_locations, entrances=south_dw_entrances) + districts['Northwest Dark World'] = District('Northwest Dark World', voo_north_locations, + entrances=voo_north_entrances) + districts['The Mire'] = District('The Mire', set(), entrances=mire_entrances) + districts['Dark Death Mountain'] = District('Dark Death Mountain', ddm_locations, entrances=ddm_entrances) + districts.update({x: District(x, set(), dungeon=x) for x in dungeon_table.keys()}) + + world.districts[player] = districts + + +def resolve_districts(world): + create_districts(world) + state = CollectionState(world) + state.sweep_for_events() + for player in range(1, world.players + 1): + check_set = find_reachable_locations(state, player) + used_locations = {l for d in world.districts[player].values() for l in d.locations} + for name, district in world.districts[player].items(): + if district.dungeon: + layout = world.dungeon_layouts[player][district.dungeon] + district.locations.update([l.name for r in layout.master_sector.regions + for l in r.locations if not l.item and l.real]) + else: + for entrance in district.entrances: + ent = world.get_entrance(entrance, player) + queue = deque([ent.connected_region]) + visited = set() + while len(queue) > 0: + region = queue.pop() + visited.add(region) + if region.type == RegionType.Cave: + for location in region.locations: + if location.name not in used_locations and not location.item and location.real: + district.locations.add(location.name) + used_locations.add(location.name) + for ext in region.exits: + if ext.connected_region not in visited: + queue.appendleft(ext.connected_region) + district.sphere_one = len(check_set.intersection(district.locations)) > 0 + + +def find_reachable_locations(state, player): + check_set = set() + for region in state.reachable_regions[player]: + for location in region.locations: + if location.can_reach(state) and not location.forced_item and location.real: + check_set.add(location.name) + return check_set From dc7d4940d99a6f1334a1f832020a07d6e9984c0a Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Oct 2021 16:29:09 -0600 Subject: [PATCH 017/293] Overworld map code --- BaseClasses.py | 6 + CLI.py | 3 +- DoorShuffle.py | 1 + Dungeons.py | 32 +++-- EntranceShuffle.py | 136 +++++++++++++++++- Items.py | 2 +- Main.py | 1 + Mystery.py | 2 + Regions.py | 20 +-- Rom.py | 54 +++++-- data/base2current.bps | Bin 136122 -> 136369 bytes mystery_example.yml | 4 + resources/app/cli/args.json | 7 + resources/app/cli/lang/en.json | 3 + resources/app/gui/lang/en.json | 4 + .../app/gui/randomize/entrando/widgets.json | 13 +- source/classes/constants.py | 3 +- 17 files changed, 250 insertions(+), 41 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 58530558..c3c9b59e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1957,6 +1957,12 @@ class Portal(object): self.dependent = None self.deadEnd = False self.light_world = False + self.chosen = False + + def find_portal_entrance(self): + p_region = self.door.entrance.connected_region + return next((x for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]), None) def change_boss_exit(self, exit_idx): self.default = False diff --git a/CLI.py b/CLI.py index bb9ab0a2..168008ae 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'overworld_map', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -146,6 +146,7 @@ def parse_settings(): "shuffleganon": True, "shuffle": "vanilla", "shufflelinks": False, + "overworld_map": "default", "pseudoboots": False, "shufflepots": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index 3d3a6eda..3e4bfac9 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1278,6 +1278,7 @@ def refine_boss_exits(world, player): if 0 < len(filtered) < len(reachable_portals): reachable_portals = filtered chosen_one = random.choice(reachable_portals) if len(reachable_portals) > 1 else reachable_portals[0] + chosen_one.chosen = True if chosen_one != current_boss: chosen_one.change_boss_exit(current_boss.boss_exit_idx) current_boss.change_boss_exit(-1) diff --git a/Dungeons.py b/Dungeons.py index 6fe38cfb..fb081155 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -378,8 +378,8 @@ flexible_starts = { class DungeonInfo: - def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): - # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize, midx): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): self.free_items = free self.key_num = keys self.bk_present = bk @@ -389,21 +389,23 @@ class DungeonInfo: self.key_drops = drops self.prize = prize + self.map_index = midx + dungeon_table = { - 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), - 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), - 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), - 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), - 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), - 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), - 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), - 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), - 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), - 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), - 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), - 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), - 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), + '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), } diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 7e5dcdd3..7a7d3116 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3733,8 +3733,6 @@ indirect_connections = { # | ([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 - # 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)), 'Inverted Big Bomb Shop': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), @@ -4034,3 +4032,137 @@ exit_ids = {'Links House Exit': (0x01, 0x00), 'Skull Pinball': 0x78, 'Skull Pot Circle': 0x76, 'Pyramid': 0x7B} + +ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Inverted Big Bomb Shop': (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)': (0x7b0, 0x730), 'Hyrule Castle Entrance (West)': (0x700, 0x640), + 'Hyrule Castle Entrance (East)': (0x8a0, 0x640), 'Inverted Pyramid Entrance': (0x720, 0x700), + 'Agahnims Tower': (0x7e0, 0x640), 'Inverted Ganons Tower': (0x7e0, 0x640), + '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': (0x850, 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), + 'Inverted Agahnims 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': None, # can't mark this one technically + 'Chicken House': (0x120, 0x880), + 'Aginahs Cave': (0x2e0, 0xd00), + 'Sahasrahlas Hut': (0xcf0, 0x6c0), + 'Cave Shop (Lake Hylia)': (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)': (0x800, 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': (0x800, 0x7a0), + 'Dark World Hammer Peg Cave': (0x4c0, 0x940), + 'Red Shield Shop': (0x500, 0x680), + 'Dark Sanctuary Hint': (0x720, 0x4a0), + 'Inverted Dark Sanctuary': (0x720, 0x4a0), + 'Fortune Teller (Dark)': (0x2c0, 0x4c0), + 'Dark World Shop': (0x2e0, 0x880), + 'Dark World Lumberjack Shop': (0x4e0, 0x0d0), + 'Dark World Potion Shop': (0xc80, 0x4c0), + 'Archery Game': (0x2f0, 0xaf0), + 'Mire Shed': (0x060, 0xc90), + 'Dark Desert Hint': (0x2e0, 0xd00), + 'Dark Desert Fairy': (0x1c0, 0xc90), + 'Spike Cave': (0x860, 0x180), + 'Cave Shop (Dark Death Mountain)': (0xd80, 0x180), + 'Dark Death Mountain Fairy': (0x620, 0x2c0), + 'Mimic Cave': (0xc80, 0x180), + 'Big Bomb Shop': (0x8b1, 0xb2d), 'Inverted Links House': (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/Items.py b/Items.py index 808a0740..ba85e51f 100644 --- a/Items.py +++ b/Items.py @@ -22,7 +22,7 @@ def ItemFactory(items, player): return ret -# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) +# Format: Name: (Advancement, Priority, Type, ItemCode, BasePrice, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), diff --git a/Main.py b/Main.py index cfd79062..1f11f3cc 100644 --- a/Main.py +++ b/Main.py @@ -104,6 +104,7 @@ def main(args, seed=None, fish=None): world.treasure_hunt_total = args.triforce_pool.copy() world.shufflelinks = args.shufflelinks.copy() world.pseudoboots = args.pseudoboots.copy() + world.overworld_map = args.overworld_map.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} diff --git a/Mystery.py b/Mystery.py index 73644500..3ab32406 100644 --- a/Mystery.py +++ b/Mystery.py @@ -135,6 +135,8 @@ def roll_settings(weights): entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' + overworld_map = get_choice('overworld_map') + ret.overworld_map = overworld_map if overworld_map != 'default' else 'default' door_shuffle = get_choice('door_shuffle') ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') diff --git a/Regions.py b/Regions.py index 546cd49b..c21953f6 100644 --- a/Regions.py +++ b/Regions.py @@ -1335,16 +1335,16 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Ice Block Drop': (None, None, False, None), 'Zelda Pickup': (None, None, False, None), 'Zelda Drop Off': (None, None, False, None), - 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'), - 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'), - 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'), - 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'), - 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'), - 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'), - 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'), - 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'), - 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'), - 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock'), + 'Eastern Palace - Prize': ([0x1209D, 0x53E76, 0x53E77, 0x180052, 0x180070, 0xC6FE], None, True, 'Eastern Palace'), + 'Desert Palace - Prize': ([0x1209E, 0x53E7A, 0x53E7B, 0x180053, 0x180072, 0xC6FF], None, True, 'Desert Palace'), + 'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706], None, True, 'Tower of Hera'), + 'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702], None, True, 'Palace of Darkness'), + 'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701], None, True, 'Swamp Palace'), + 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707], None, True, 'Thieves\' Town'), + 'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704], None, True, 'Skull Woods'), + 'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705], None, True, 'Ice Palace'), + 'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703], None, True, 'Misery Mire'), + 'Turtle Rock - Prize': ([0x120A7, 0x53E80, 0x53E81, 0x18005C, 0x180075, 0xC708], None, True, 'Turtle Rock'), '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 d8514556..ccad9d13 100644 --- a/Rom.py +++ b/Rom.py @@ -16,8 +16,8 @@ except ImportError: raise Exception('Could not load BPS module') from BaseClasses import CollectionState, ShopType, Region, Location, Door, DoorType, RegionType, PotItem -from DoorShuffle import compass_data, DROptions, boss_indicator -from Dungeons import dungeon_music_addresses +from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals +from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops from RoomData import DoorKind from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable @@ -26,13 +26,13 @@ from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, 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 +from EntranceShuffle import door_addresses, exit_ids, ow_prize_table from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '1c59cec98ba4555db8eed1d2dea76497' +RANDOMIZERBASEHASH = '7ec52e136e8c73a9e093a4baa43fc2d2' class JsonRom(object): @@ -1401,14 +1401,48 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count + compass_mode = 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': - rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location - elif world.dungeon_counters[player] == 'on': - rom.write_byte(0x18003C, 0x02) # always on - elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup': - rom.write_byte(0x18003C, 0x01) # show on pickup - else: + compass_mode = 0x00 # Currently must be off if timer is on, because they use same HUD location rom.write_byte(0x18003C, 0x00) + elif world.dungeon_counters[player] == 'on': + compass_mode = 0x02 # always on + elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup': + compass_mode = 0x01 # show on pickup + if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': + compass_mode |= 0x80 # turn on locating dungeons + 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)) + if world.compassshuffle[player] and world.overworld_map[player] == 'compass': + compass_mode |= 0x40 # compasses are wild + for dungeon, portal_list in dungeon_portals.items(): + ow_map_index = dungeon_table[dungeon].map_index + if len(portal_list) == 1: + portal_idx = 0 + else: + if world.doorShuffle[player] == 'crossed': + # 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] + portal = world.get_portal(portal_list[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) + 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) + # in crossed doors - flip the compass exists flags + if world.doorShuffle[player] == 'crossed': + 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) # Bitfield - enable free items to show up in menu # diff --git a/data/base2current.bps b/data/base2current.bps index fca7a6ecdc7d2b97a0a85bb86dd89a16dfb4749c..0e2a33a60a727bdf791e67f15a7a564fb7eb477b 100644 GIT binary patch delta 9261 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%0DhXFh(4rE7Z_WsuHYfs5#FVDs<`!|ans*s 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 Date: Wed, 27 Oct 2021 15:02:33 -0600 Subject: [PATCH 018/293] Mystery test suite --- .gitignore | 2 +- Mystery.py | 2 + mystery_testsuite.yml | 164 ++++++++++++++++++++++++++++++++ source/test/MysteryTestSuite.py | 124 ++++++++++++++++++++++++ source/test/__init__.py | 0 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 mystery_testsuite.yml create mode 100644 source/test/MysteryTestSuite.py create mode 100644 source/test/__init__.py diff --git a/.gitignore b/.gitignore index bee1283a..b36abdb5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,6 @@ resources/user/* get-pip.py venv -test +test_games/ data/sprites/official/selan.1.zspr *.zspr diff --git a/Mystery.py b/Mystery.py index 3ab32406..6151693e 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,6 +28,7 @@ def main(): 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('--suppress_rom', action='store_true') parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') @@ -61,6 +62,7 @@ def main(): erargs.seed = seed erargs.names = args.names erargs.create_spoiler = args.create_spoiler + erargs.suppress_rom = args.suppress_rom erargs.race = True erargs.outputname = seedname erargs.outputpath = args.outputpath diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml new file mode 100644 index 00000000..fa99cf91 --- /dev/null +++ b/mystery_testsuite.yml @@ -0,0 +1,164 @@ +description: A test suite for testing various combinations +# Not yet in this branch +#algorithm: +# major_only: 1 +# dungeon_only: 1 +# vanilla_fill: 1 +# balanced: 10 +# district: 1 +door_shuffle: + vanilla: 1 + basic: 2 + crossed: 3 # crossed yield more errors so is preferred +intensity: + 1: 1 + 2: 1 + 3: 2 # intensity 3 usuall yield more errors +keydropshuffle: + on: 1 + off: 1 +shopsanity: + on: 1 + off: 1 +pot_shuffle: + on: 1 + off: 1 +entrance_shuffle: + none: 1 + dungeonssimple: 1 + dungeonsfull: 1 + simple: 1 + restricted: 1 + full: 1 + crossed: 1 + insanity: 1 +shufflelinks: + on: 1 + off: 1 +world_state: + standard: 1 + open: 1 + inverted: 1 + retro: 0 +retro: + on: 1 + off: 1 +goals: + ganon: 1 + fast_ganon: 1 + dungeons: 2 # this yields more errors so is preferred + pedestal: 1 + triforce-hunt: 1 +triforce_goal_min: 20 +triforce_goal_max: 30 +triforce_pool_min: 30 +triforce_pool_max: 40 +triforce_min_difference: 10 +map_shuffle: + on: 1 + off: 1 +compass_shuffle: + on: 1 + off: 1 +smallkey_shuffle: + on: 1 + off: 1 +bigkey_shuffle: + on: 1 + off: 1 +dungeon_counters: + on: 1 + off: 1 + default: 1 +experimental: + on: 1 + off: 1 +glitches_required: + none: 10 # i'm more interest in testing shuffles with more restrictive logic + owg: 1 + no_logic: 1 +accessibility: + items: 1 + locations: 1 + none: 0 # i'm not really interested in this yet +restrict_boss_items: + none: 1 + mapcompass: 1 + dungeon: 1 +tower_open: + "0": 1 + "1": 1 + "2": 1 + "3": 1 + "4": 1 + "5": 1 + "6": 1 + "7": 10 # more restrictions is usually best for testing + random: 1 +ganon_open: + "0": 1 + "1": 1 + "2": 1 + "3": 1 + "4": 1 + "5": 1 + "6": 1 + "7": 10 # more restrictions is usually best for testing + random: 1 +boss_shuffle: + none: 1 + simple: 1 + full: 1 + random: 1 +enemy_shuffle: # shouldn't affect generation + none: 1 + shuffled: 1 + random: 1 + legacy: 0 +hints: + on: 1 + off: 1 +pseudoboots: # shouldn't affect generation + on: 1 + off: 1 +weapons: + randomized: 1 + assured: 1 + vanilla: 1 + swordless: 1 +item_pool: + normal: 1 + hard: 1 + expert: 1 +item_functionality: # shouldn't affect generation + normal: 1 + hard: 0 + expert: 0 +enemy_damage: # shouldn't affect generation + default: 1 + shuffled: 0 + random: 0 +enemy_health: # shouldn't affect generation + default: 1 + easy: 0 + hard: 0 + expert: 0 +rom: + quickswap: # shouldn't affect generation + on: 1 + off: 0 +# reduce_flashing: should affect generation at this point + heartcolor: # shouldn't affect generation + red: 1 + blue: 1 + green: 1 + yellow: 1 + heartbeep: # shouldn't affect generation + double: 0 + normal: 0 + half: 0 + quarter: 1 + off: 0 + shuffle_sfx: + on: 1 + off: 1 diff --git a/source/test/MysteryTestSuite.py b/source/test/MysteryTestSuite.py new file mode 100644 index 00000000..b5143399 --- /dev/null +++ b/source/test/MysteryTestSuite.py @@ -0,0 +1,124 @@ +import subprocess +import sys +import multiprocessing +import concurrent.futures +import argparse +from collections import OrderedDict + +cpu_threads = multiprocessing.cpu_count() +py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + +def main(args=None): + successes = [] + errors = [] + task_mapping = [] + tests = OrderedDict() + + successes.append(f"Testing {args.dr} DR with {args.count} Tests" + (f" (intensity={args.tense})" if args.dr in ['basic', 'crossed'] else "")) + print(successes[0]) + + max_attempts = args.count + pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads) + dead_or_alive = 0 + alive = 0 + + def test(testname: str, command: str): + tests[testname] = [command] + basecommand = f"python3.8 Mystery.py --suppress_rom" + + def gen_seed(): + taskcommand = basecommand + " " + command + return subprocess.run(taskcommand, capture_output=True, shell=True, text=True) + + for x in range(1, max_attempts + 1): + task = pool.submit(gen_seed) + task.success = False + task.name = testname + task.mode = "Mystery" + task.cmd = basecommand + " " + command + task_mapping.append(task) + + for i in range(0, 100): + test("Mystery", "--weights mystery_testsuite.yml") + + from tqdm import tqdm + with tqdm(concurrent.futures.as_completed(task_mapping), + total=len(task_mapping), unit="seed(s)", + desc=f"Success rate: 0.00%") as progressbar: + for task in progressbar: + dead_or_alive += 1 + try: + result = task.result() + if result.returncode: + errors.append([task.name, task.cmd, result.stderr]) + else: + alive += 1 + task.success = True + except Exception as e: + raise e + + progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}") + + def get_results(testname: str): + result = "" + for mode in ['Mystery']: + dead_or_alive = [task.success for task in task_mapping if task.name == testname and task.mode == mode] + alive = [x for x in dead_or_alive if x] + success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%" + successes.append(success) + print(success) + result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t" + return result.strip() + + results = [] + for t in tests.keys(): + results.append(get_results(t)) + + for result in results: + print(result) + successes.append(result) + + return successes, errors + + +if __name__ == "__main__": + successes = [] + + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0)) + parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1)) + parser.add_argument('--help', default=False, action='store_true') + + args = parser.parse_args() + + if args.help: + parser.print_help() + exit(0) + + cpu_threads = args.cpu_threads + + for dr in [['mystery', args.count if args.count else 1, 1]]: + + for tense in range(1, dr[2] + 1): + args = argparse.Namespace() + args.dr = dr[0] + args.tense = tense + args.count = dr[1] + s, errors = main(args=args) + if successes: + successes += [""] * 2 + successes += s + print() + + if errors: + with open(f"{dr[0]}{(f'-{tense}' if dr[0] in ['basic', 'crossed'] else '')}-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") + + with open("success.txt", "w") as stream: + stream.write(str.join("\n", successes)) + + input("Press enter to continue") diff --git a/source/test/__init__.py b/source/test/__init__.py new file mode 100644 index 00000000..e69de29b From 4131896f93ca5ecfeea0ca0819d569e46d2fd1a7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 2 Nov 2021 15:40:15 -0600 Subject: [PATCH 019/293] Release notes for restricted boss item --- Main.py | 2 +- RELEASENOTES.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 83dc1021..447c3682 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config -__version__ = '1.0.1.0-v' +__version__ = '1.0.2.0-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fc5ca90f..e4fd57f8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,26 @@ If you do not shuffle the compass or map outside of the dungeon, the non-shuffle The map item plays double duty in this mode and only possession of the map will show both prize and location of the dungeon. If you do not shuffle maps or the dungeon does not have a map, the information will be displayed without needing to find any items. +## Restricted Dungeon Items on Bosses + +You may now restrict the items that can appear on the boss, like the popular ambrosia preset does. + +CLI: ```--restrict_boss_items

XI{>4l`+1PVCl^_;7dsST;Ln5g>%Yk$xzE`N)>7; zFfLAnvR}d%C)4@^IdvFTqO1;7pLlW~Bs@cL#xi&~!J^dtCXYIdAfVXjI^C@Y&sw z5$ml#(CUQ5O^DqRjrx{n(Nuh;5AQtt2(#&t`zr6)^Il^!}@ z;uG;EpGMS^lcS_Jyv{e`8QSx{ZJ~VAoBVZRtXyd6?WLO-{|V)A&7|_?er=&<3?00= zsJZbk81f~prZ?7S<9A{!h_vxDNO%pE#o^%%Z$x< zUgOOt^vyVd$(sx5dr{tQHfBbAd3DU3=3i6Y;H7)xef@Oi@HhDZx#LmN?E{P9+L2Il ze-{eu>ie-E1zx^imA281_(gHm=~6o9fW@sQ%}0La8vgXnV7cdYQ)1korl^E-x)*A( z<=WYe!<{f}`RN8(>;>{|{fNMtOC3p=fM1JVUJ`yX5o>cnnWpDBc;EpM&~|-)QLOxX zRE|>KDG697$C_A7e(^e-_Hc~n_tyhfKT=8@DGI?kPFTtMhtY=Ycm-T(t!%Bx86>rD|e+e z>d!eI?$dT_eDFcW6+v+4fA@o$hYFpZr<%)LL(c8NphscVMa+KwK)_8YfLq><7W}G% zO>f7$Wb8Jkr^6?2n}hm!5Qd=WJ>=5r&vA7vW>4dLoDt61%#Ijq^SdY@*2cZ87EP-9 z8$Re+9j5lmu2_5@PCTX8Us8Kyp9zIICrclaZgf%U@WmoQT$%~K7* z_Go7A4^^&qkj-x}4)K7x|D(Wm81kV%m|z|Ip#{jy{;f&Q2)oD<5n1tN4B<_{|Lctb zi>pQge-KtR1BA&I5o#13YlfAgC*Wvnef1to9>`pE+;%mi@!QO_6lOL38>l%&iwg u9Tyt6hqRQ&g2;Y8!<*D{tU7a Date: Tue, 7 Jun 2022 08:10:19 -0600 Subject: [PATCH 181/293] Settings code fix --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 4b3986bf..1d90ed37 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2893,7 +2893,7 @@ class Settings(object): args.bombbag[p] = True if settings[8] & 0x2 else False args.shufflelinks[p] = True if settings[8] & 0x1 else False if len(settings) > 9: - args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0x80) >> 6] + args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6] args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] From e9433e56c076fa6d0a3b1aeb1e48a17a27cd6d79 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 7 Jun 2022 08:12:13 -0600 Subject: [PATCH 182/293] Missing player --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 836a164e..1c9d82a7 100644 --- a/Rom.py +++ b/Rom.py @@ -1274,7 +1274,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest if world.open_pyramid[player] or world.goal[player] == 'trinity': rom.initial_sram.pre_open_pyramid_hole() - if world.crystals_needed_for_gt == 0: + if world.crystals_needed_for_gt[player] == 0: rom.initial_sram.pre_open_ganons_tower() rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable From 483c32365b70c67e19bb7daaf7ffd5ba69cfae47 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 10 Jun 2022 17:46:05 -0500 Subject: [PATCH 183/293] Minor capitalization --- Items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Items.py b/Items.py index bc685236..dfb19e19 100644 --- a/Items.py +++ b/Items.py @@ -42,7 +42,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'), 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), - 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), + 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), From d9d7a2afab34edebf51854ee52a39ddfa622a294 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 10 Jun 2022 17:53:29 -0500 Subject: [PATCH 184/293] Version bump 0.2.7.3 --- CHANGELOG.md | 10 +++++++++- OverworldShuffle.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae14e2cf..dbb65f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ # Changelog +### 0.2.7.3 +- Restructured OWR algorithm to include some additional scenarios not previously allowed +- Added new Inverted D-pad controls for Social Distorion (ie. Mirror Mode) support +- Crossed OWR/Special OW Areas are now included in the spoiler log +- Fixed default TF pieces with Trinity in Mystery +- Added bush crabs to rupee farm logic (only in non-enemizer) +- Updated tree pull logic to also require ability to kill most things + ### 0.2.7.2 -- Special OW Area are now shuffled in Layout Shuffle (Zora/Hobo/Pedestal) +- Special OW Areas are now shuffled in Layout Shuffle (Zora/Hobo/Pedestal) - Fixed some broken water region graph modelling, fixed some reachability logic - Some minor code simplifications diff --git a/OverworldShuffle.py b/OverworldShuffle.py index c5485643..287961be 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.7.2' +version_number = '0.2.7.3' version_branch = '-u' __version__ = '%s%s' % (version_number, version_branch) From 25905fe3f38a09511d2bf25e22f58a829d3d7ffc Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Jun 2022 13:22:24 -0600 Subject: [PATCH 185/293] Capitalization fixes Highlight part of the text in red (experimental) --- Items.py | 78 ++++++++++++++++++++++++------------------------- Main.py | 2 +- RELEASENOTES.md | 5 ++++ Rom.py | 49 ++++++++++++++++++++----------- Text.py | 37 +++++++++++++++++++++-- 5 files changed, 112 insertions(+), 59 deletions(-) diff --git a/Items.py b/Items.py index 06e1dcec..90b518cb 100644 --- a/Items.py +++ b/Items.py @@ -23,51 +23,51 @@ def ItemFactory(items, player): # Format: Name: (Advancement, Priority, Type, ItemCode, BasePrice, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) -item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), - 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), - 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), - 'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), - 'Hammer': (True, False, None, 0x09, 250, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the Hammer'), +item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the bow'), + 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a bow'), + 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a bow'), + 'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the book'), + 'Hammer': (True, False, None, 0x09, 250, 'Stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), 'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), - 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'), - 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), - 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'), + 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the mirror'), + 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), + 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the boots'), 'Power Glove': (True, False, None, 0x1B, 100, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), - 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the Cape'), - 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'), - 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'), - 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'), - 'Magic Powder': (True, False, None, 0x0D, 50, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'), + 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'), + 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the mushroom'), + 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the shovel'), + 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the lamp'), + 'Magic Powder': (True, False, None, 0x0D, 50, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the powder'), 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), - 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red Cane'), + 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red cane'), 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), - 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), + 'Flippers': (True, False, None, 0x1E, 250, 'Fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), 'Ether': (True, False, None, 0x10, 100, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), 'Quake': (True, False, None, 0x11, 100, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), - 'Bottle': (True, False, None, 0x16, 50, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'), - 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'), - 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a Bottle'), - 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a Bottle'), - 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'), - 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a Bottle'), - 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a Bottle'), + 'Bottle': (True, False, None, 0x16, 50, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'), + 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'), + 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'), + 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'), + 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'), + 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'), + 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'), 'Master Sword': (True, False, 'Sword', 0x50, 100, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the small sword'), 'Sword and Shield': (True, False, 'Sword', 0x00, 'An uncle\nsword rests\nhere!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), - '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'), + '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, 0x01, 0x08], 999, None, None, None, None, None, None, None), 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x09], 999, None, None, None, None, None, None, None), 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x0a], 999, None, None, None, None, None, None, None), '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'), + '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, 0x06, 0x01], 999, None, None, None, None, None, None, None), 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x02], 999, None, None, None, None, None, None, None), 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x03], 999, None, None, None, None, None, None, None), @@ -75,10 +75,10 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x05], 999, None, None, None, None, None, None, None), 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x07], 999, None, None, None, None, None, None, None), - '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'), + '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'), - 'Arrow Upgrade (+5)': (False, False, None, 0x53, 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'), + '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'), + 'Arrow Upgrade (+5)': (False, False, None, 0x53, 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'), 'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), 'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), 'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), @@ -86,17 +86,17 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Chicken': (False, False, None, 0x5A, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), 'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), 'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), - 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), - 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), 'Blue Mail': (False, True, None, 0x22, 50, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the blue mail'), 'Red Mail': (False, True, None, 0x23, 100, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the red mail'), - 'Progressive Armor': (False, True, None, 0x60, 50, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), + 'Progressive Armor': (False, True, None, 0x60, 50, 'Time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), 'Blue Boomerang': (True, False, None, 0x0C, 50, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the blue boomerang'), 'Red Boomerang': (True, False, None, 0x2A, 50, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the red boomerang'), 'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a blue shield'), 'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'a red shield'), 'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), - 'Progressive Shield': (True, False, None, 0x5F, 50, 'have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), + 'Progressive Shield': (True, False, None, 0x5F, 50, 'Have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), 'Bug Catching Net': (True, False, None, 0x21, 50, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the bug net'), 'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the blue Cane'), 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), @@ -108,12 +108,12 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Rupees (50)': (False, False, None, 0x41, 25, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'), 'Rupees (100)': (False, False, None, 0x40, 50, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'), 'Rupees (300)': (False, False, None, 0x46, 150, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'), - 'Rupoor': (False, False, None, 0x59, 0, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), - 'Red Clock': (False, True, None, 0x5B, 0, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), - 'Blue Clock': (False, True, None, 0x5C, 50, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), - 'Green Clock': (False, True, None, 0x5D, 200, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), - 'Single RNG': (False, True, None, 0x62, 300, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'), - 'Multi RNG': (False, True, None, 0x63, 100, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), + 'Rupoor': (False, False, None, 0x59, 0, 'A debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), + 'Red Clock': (False, True, None, 0x5B, 0, 'A waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), + 'Blue Clock': (False, True, None, 0x5C, 50, 'A bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), + 'Green Clock': (False, True, None, 0x5D, 200, 'A lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), + 'Single RNG': (False, True, None, 0x62, 300, 'Something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'), + 'Multi RNG': (False, True, None, 0x63, 100, 'Something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'), diff --git a/Main.py b/Main.py index f3dcb0ee..174a2568 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.2.4-v' +__version__ = '1.0.2.5-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 31d1e10e..f120a32c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -157,6 +157,11 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile +* 1.0.2.5 + * Some textual changes for hints (capitalization standardization) + * Item will be highlighted in red if experimental is on + * Bug with 0 GT crystals not opening GT + * Settings code fix * 1.0.2.4 * Updated tourney winners (included Doors Async League winners) * Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking diff --git a/Rom.py b/Rom.py index 1c9d82a7..f4bdd02c 100644 --- a/Rom.py +++ b/Rom.py @@ -15,7 +15,7 @@ try: except ImportError: raise Exception('Could not load BPS module') -from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType +from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType, Item from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops @@ -1975,6 +1975,8 @@ def write_strings(rom, world, player, team): else: if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon: hint = dest.dungeon.name + elif isinstance(dest, Item) and world.experimental[player]: + hint = f'{{C:RED}}{dest.hint_text}{{C:WHITE}}' if dest.hint_text else 'something' else: hint = dest.hint_text if dest.hint_text else "something" if dest.player != player: @@ -1994,7 +1996,7 @@ def write_strings(rom, world, player, team): all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player] random.shuffle(all_entrances) - #First we take care of the one inconvenient dungeon in the appropriately simple shuffles. + # First we take care of the one inconvenient dungeon in the appropriately simple shuffles. entrances_to_hint = {} entrances_to_hint.update(InconvenientDungeonEntrances) if world.shuffle_ganon: @@ -2009,7 +2011,7 @@ def write_strings(rom, world, player, team): tt[hint_locations.pop(0)] = this_hint entrances_to_hint = {} break - #Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. + # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. entrances_to_hint.update(InconvenientOtherEntrances) if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: hint_count = 0 @@ -2027,7 +2029,7 @@ def write_strings(rom, world, player, team): else: break - #Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. + # Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(DungeonEntrances) @@ -2082,7 +2084,7 @@ def write_strings(rom, world, player, team): else: second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) - this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.') + this_hint = f'The westmost chests in Swamp Palace contain {first_item} and {second_item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Mire Left': if random.randint(0, 1) == 0: @@ -2091,34 +2093,43 @@ def write_strings(rom, world, player, team): else: second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) - this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.') + this_hint = f'The westmost chests in Misery Mire contain {first_item} and {second_item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Tower of Hera - Big Key Chest': - this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'Waiting in the Tower of Hera basement leads to {item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Ganons Tower - Big Chest': - this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'The big chest in Ganon\'s Tower contains {item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Thieves\' Town - Big Chest': - this_hint = 'The big chest in Thieves\' Town contains ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'The big chest in Thieves\' Town contains {item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Ice Palace - Big Chest': - this_hint = 'The big chest in Ice Palace contains ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'The big chest in Ice Palace contains {item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Eastern Palace - Big Key Chest': - this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'The antifairy guarded chest in Eastern Palace contains {item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Sahasrahla': - this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'Sahasrahla seeks a green pendant for {item}.' tt[hint_locations.pop(0)] = this_hint elif location == 'Graveyard Cave': - this_hint = 'The cave north of the graveyard contains ' + hint_text(world.get_location(location, player).item) + '.' + item = hint_text(world.get_location(location, player).item) + this_hint = f'The cave north of the graveyard contains {item}.' tt[hint_locations.pop(0)] = this_hint else: - this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' + this_hint = f'{location} contains {hint_text(world.get_location(location, player).item)}.' tt[hint_locations.pop(0)] = this_hint - # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. + # Lastly we write hints to show where certain interesting items are. + # It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless + # of how many exist. This supports many settings well. items_to_hint = RelevantItems.copy() if world.keyshuffle[player]: items_to_hint.extend(SmallKeys) @@ -2132,7 +2143,10 @@ def write_strings(rom, world, player, team): this_location = world.find_items_not_key_only(this_item, player) random.shuffle(this_location) if this_location: - this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' + item_name = this_location[0].item.hint_text + item_name = item_name[0].upper() + item_name[1:] + item_format = f'{{C:RED}}{item_name}{{C:WHITE}}' if world.experimental[player] else item_name + this_hint = f'{item_format} can be found {hint_text(this_location[0])}.' tt[hint_locations.pop(0)] = this_hint hint_count -= 1 @@ -2186,7 +2200,8 @@ def write_strings(rom, world, player, team): elif hint_type == 'path': if item_count == 1: the_item = text_for_item(next(iter(choice_set)), world, player, team) - hint_candidates.append((hint_type, f'{name} conceals {the_item}')) + item_format = f'{{C:RED}}{the_item}{{C:WHITE}}' if world.experimental[player] else the_item + hint_candidates.append((hint_type, f'{name} conceals only {item_format}')) else: hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items')) district_hints = min(len(hint_candidates), len(hint_locations)) diff --git a/Text.py b/Text.py index 07b076f9..4da86b4c 100644 --- a/Text.py +++ b/Text.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- from collections import OrderedDict import logging +import re text_addresses = {'Pedestal': (0x180300, 256), 'Triforce': (0x180400, 256), @@ -624,6 +625,12 @@ class MultiByteCoreTextMapper(object): "{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03], "{C:GREEN}": [0x77, 0x07], "{C:YELLOW}": [0x77, 0x02], + "{C:WHITE}": [0x77, 0x06], + "{C:INV_WHITE}": [0x77, 0x16], + "{C:INV_YELLOW}": [0x77, 0x12], + "{C:INV_GREEN}": [0x77, 0x17], + "{C:RED}": [0x77, 0x01], + "{C:INV_RED}": [0x77, 0x11], } @classmethod @@ -637,7 +644,9 @@ class MultiByteCoreTextMapper(object): while lines: linespace = wrap line = lines.pop(0) - if line.startswith('{'): + + match = re.search('^\{[A-Z0-9_:]+\}$', line) + if match: if line == '{PAGEBREAK}': if lineindex % 3 != 0: # insert a wait for keypress, unless we just did so @@ -654,10 +663,27 @@ class MultiByteCoreTextMapper(object): pending_space = False while words: word = words.pop(0) + + match = re.search('^(\{[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) + if match: + end_command = match.group(1) + word = word.replace(end_command, '') + period = word.endswith('.') + else: + end_command, period = None, False + # sanity check: if the word we have is more than 19 characters, # we take as much as we can still fit and push the rest back for later if cls.wordlen(word) > wrap: (word_first, word_rest) = cls.splitword(word, linespace) + if end_command: + word_rest = (word_rest[:-1] + end_command + '.') if period else (word_rest + end_command) words.insert(0, word_rest) lines.insert(0, ' '.join(words)) @@ -670,9 +696,16 @@ class MultiByteCoreTextMapper(object): if cls.wordlen(word) < linespace: pending_space = True linespace -= cls.wordlen(word) + 1 if pending_space else 0 - outbuf.extend(RawMBTextMapper.convert(word)) + word_to_map = word[:-1] if period else word + outbuf.extend(RawMBTextMapper.convert(word_to_map)) + if end_command: + outbuf.extend(cls.special_commands[end_command]) + if period: + outbuf.extend(RawMBTextMapper.convert('.')) else: # ran out of space, push word and lines back and continue with next line + if end_command: + word = (word[:-1] + end_command + '.') if period else (word + end_command) words.insert(0, word) lines.insert(0, ' '.join(words)) break From 32aebb2ad141a11efca033b45206b1551c87e599 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Jun 2022 16:55:20 -0600 Subject: [PATCH 186/293] Pottery fix for item counting because of shop conflict --- RELEASENOTES.md | 1 + Rom.py | 2 +- data/base2current.bps | Bin 93021 -> 93041 bytes 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f120a32c..a27c05d0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -162,6 +162,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Item will be highlighted in red if experimental is on * Bug with 0 GT crystals not opening GT * Settings code fix + * Fix for pottery not counting items in certain caves that share a supertile with shops * 1.0.2.4 * Updated tourney winners (included Doors Async League winners) * Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking diff --git a/Rom.py b/Rom.py index f4bdd02c..c64c7491 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '01166fb16b38b49ef79acc9993dc4f02' +RANDOMIZERBASEHASH = 'e33204b6023f07025eba16874308f57d' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index bd6417a47ef665eabc70d7f6055e50cdcb4562a4..fc22d9c3956780075f6752188bd52fca2ae6c189 100644 GIT binary patch delta 3217 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 LP#1RGTsixHG@5?( delta 3280 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 fWKz-Q^6O6{O4j?rcrZGc Date: Thu, 16 Jun 2022 06:58:41 -0600 Subject: [PATCH 187/293] Fix follower despawn when entering maiden's cell and she's not there due to Blind not being the boss --- Rom.py | 3 +++ test/customizer/std_maidencell_hc.yaml | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 test/customizer/std_maidencell_hc.yaml diff --git a/Rom.py b/Rom.py index c64c7491..b7015162 100644 --- a/Rom.py +++ b/Rom.py @@ -353,6 +353,9 @@ def patch_enemizer(world, player, rom, local_rom, enemizercli, random_sprite_on_ 0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d]) rom.write_byte(0x200101, 0) # Do not close boss room door on entry. rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. (for Ijwu's enemizer) + else: + rom.write_byte(0x04DE83, 0xB3) # maiden is now something else + if random_sprite_on_hit: _populate_sprite_table() diff --git a/test/customizer/std_maidencell_hc.yaml b/test/customizer/std_maidencell_hc.yaml new file mode 100644 index 00000000..61e6fa11 --- /dev/null +++ b/test/customizer/std_maidencell_hc.yaml @@ -0,0 +1,26 @@ +meta: +# seed: 872603231 + seed: 222336029 +settings: + 1: + door_shuffle: crossed +# door_shuffle: vanilla + mode: standard + shufflebosses: unique +doors: + 1: + doors: + Hyrule Dungeon Cellblock Up Stairs: Thieves Basement Block Up Stairs +bosses: + 1: + Thieves Town: Moldorm +#start_inventory: +# 1: +# - Titans Mitts +# - Ocarina +# - Moon Pearl +# - Tempered Sword +# - Boss Heart Container +# - Boss Heart Container +# - Boss Heart Container +# - Boss Heart Container From b938bef8fac9e71c7cf15a32afbcbd70d570b7a9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 16 Jun 2022 10:46:53 -0600 Subject: [PATCH 188/293] Check package requirements before importing them --- DungeonRandomizer.py | 8 +++--- Gui.py | 4 +++ source/meta/check_requirements.py | 41 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 source/meta/check_requirements.py diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index d24e81d6..cf0f73bc 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -import argparse -import copy +if __name__ == '__main__': + from source.meta.check_requirements import check_requirements + check_requirements(console=True) + import os import logging import RaceRandom as random -import textwrap -import shlex import sys from source.classes.BabelFish import BabelFish diff --git a/Gui.py b/Gui.py index 032cce90..3c640979 100755 --- a/Gui.py +++ b/Gui.py @@ -1,3 +1,7 @@ +if __name__ == '__main__': + from source.meta.check_requirements import check_requirements + check_requirements() + import json import os import sys diff --git a/source/meta/check_requirements.py b/source/meta/check_requirements.py new file mode 100644 index 00000000..680dfe8f --- /dev/null +++ b/source/meta/check_requirements.py @@ -0,0 +1,41 @@ +import importlib.util +import webbrowser +from tkinter import Tk, Label, Button, Frame + + +def check_requirements(console=False): + check_packages = {'aenum': 'aenum', + 'fast-enum': 'fast_enum', + 'python-bps-continued': 'bps', + 'colorama': 'colorama', + 'aioconsole' : 'aioconsole', + 'websockets' : 'websockets', + 'pyyaml': 'yaml'} + missing = [] + for package, import_name in check_packages.items(): + spec = importlib.util.find_spec(import_name) + if spec is None: + missing.append(package) + if len(missing) > 0: + packages = ','.join(missing) + if console: + import logging + logger = logging.getLogger('') + logger.error('You need to install the following python packages:') + logger.error(f'{packages}') + logger.error('See the step about "Installing Platform-specific dependencies":') + logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md') + else: + master = Tk() + master.title('Error') + frame = Frame(master) + frame.pack(expand=True, padx =50, pady=50) + Label(frame, text='You need to install the following python packages:').pack() + Label(frame, text=f'{packages}').pack() + Label(frame, text='See the step about "Installing Platform-specific dependencies":').pack() + url = 'https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md' + link = Label(frame, fg='blue', cursor='hand2', text=url) + link.pack() + link.bind('', lambda e: webbrowser.open_new_tab(url)) + Button(master, text='Ok', command=master.destroy).pack() + master.mainloop() From 8d140c9bf801a21b4479ef2db86c4b9bec7fdc5e Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 17 Jun 2022 14:36:51 -0600 Subject: [PATCH 189/293] Spoiler refactor --- BaseClasses.py | 220 +++++++++++++++++++++------------ Main.py | 21 +++- resources/app/cli/lang/en.json | 1 + 3 files changed, 163 insertions(+), 79 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1d90ed37..12b5665b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2359,6 +2359,62 @@ class Spoiler(object): else: self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)]) + def parse_meta(self): + from Main import __version__ as ERVersion + + self.startinventory = list(map(str, self.world.precollected_items)) + self.metadata = {'version': ERVersion, + 'logic': self.world.logic, + 'mode': self.world.mode, + 'retro': self.world.retro, + 'bombbag': self.world.bombbag, + 'weapons': self.world.swords, + 'goal': self.world.goal, + 'shuffle': self.world.shuffle, + 'shuffleganon': self.world.shuffle_ganon, + 'shufflelinks': self.world.shufflelinks, + 'overworld_map': self.world.overworld_map, + 'door_shuffle': self.world.doorShuffle, + 'intensity': self.world.intensity, + 'dungeon_counters': self.world.dungeon_counters, + 'item_pool': self.world.difficulty, + 'item_functionality': self.world.difficulty_adjustments, + 'gt_crystals': self.world.crystals_needed_for_gt, + 'ganon_crystals': self.world.crystals_needed_for_ganon, + 'open_pyramid': self.world.open_pyramid, + 'accessibility': self.world.accessibility, + 'restricted_boss_items': self.world.restrict_boss_items, + 'hints': self.world.hints, + 'mapshuffle': self.world.mapshuffle, + 'compassshuffle': self.world.compassshuffle, + 'keyshuffle': self.world.keyshuffle, + 'bigkeyshuffle': self.world.bigkeyshuffle, + 'boss_shuffle': self.world.boss_shuffle, + 'enemy_shuffle': self.world.enemy_shuffle, + 'enemy_health': self.world.enemy_health, + 'enemy_damage': self.world.enemy_damage, + 'players': self.world.players, + 'teams': self.world.teams, + 'experimental': self.world.experimental, + 'dropshuffle': self.world.dropshuffle, + 'pottery': self.world.pottery, + 'potshuffle': self.world.potshuffle, + 'shopsanity': self.world.shopsanity, + 'pseudoboots': self.world.pseudoboots, + 'triforcegoal': self.world.treasure_hunt_count, + 'triforcepool': self.world.treasure_hunt_total, + 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} + } + + for p in range(1, self.world.players + 1): + from ItemList import set_default_triforce + if self.world.custom and p in self.world.customitemarray: + self.metadata['triforcegoal'][p], self.metadata['triforcepool'][p] = set_default_triforce(self.metadata['goal'][p], self.world.customitemarray[p]["triforcepiecesgoal"], self.world.customitemarray[p]["triforcepieces"]) + else: + custom_goal = self.world.treasure_hunt_count[p] if isinstance(self.world.treasure_hunt_count, dict) else self.world.treasure_hunt_count + custom_total = self.world.treasure_hunt_total[p] if isinstance(self.world.treasure_hunt_total, dict) else self.world.treasure_hunt_total + self.metadata['triforcegoal'][p], self.metadata['triforcepool'][p] = set_default_triforce(self.metadata['goal'][p], custom_goal, custom_total) + def parse_data(self): self.medallions = OrderedDict() if self.world.players == 1: @@ -2378,8 +2434,6 @@ 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] - self.startinventory = list(map(str, self.world.precollected_items)) - self.locations = OrderedDict() listed_locations = set() @@ -2450,47 +2504,6 @@ class Spoiler(object): for portal in self.world.dungeon_portals[player]: self.set_lobby(portal.name, portal.door.name, player) - from Main import __version__ as ERVersion - self.metadata = {'version': ERVersion, - 'logic': self.world.logic, - 'mode': self.world.mode, - 'retro': self.world.retro, - 'bombbag': self.world.bombbag, - 'weapons': self.world.swords, - 'goal': self.world.goal, - 'shuffle': self.world.shuffle, - 'shufflelinks': self.world.shufflelinks, - 'door_shuffle': self.world.doorShuffle, - 'intensity': self.world.intensity, - 'item_pool': self.world.difficulty, - 'item_functionality': self.world.difficulty_adjustments, - 'gt_crystals': self.world.crystals_needed_for_gt, - 'ganon_crystals': self.world.crystals_needed_for_ganon, - 'open_pyramid': self.world.open_pyramid, - 'accessibility': self.world.accessibility, - 'restricted_boss_items': self.world.restrict_boss_items, - 'hints': self.world.hints, - 'mapshuffle': self.world.mapshuffle, - 'compassshuffle': self.world.compassshuffle, - 'keyshuffle': self.world.keyshuffle, - 'bigkeyshuffle': self.world.bigkeyshuffle, - 'boss_shuffle': self.world.boss_shuffle, - 'enemy_shuffle': self.world.enemy_shuffle, - 'enemy_health': self.world.enemy_health, - 'enemy_damage': self.world.enemy_damage, - 'players': self.world.players, - 'teams': self.world.teams, - 'experimental': self.world.experimental, - 'dropshuffle': self.world.dropshuffle, - 'pottery': self.world.pottery, - 'potshuffle': self.world.potshuffle, - 'shopsanity': self.world.shopsanity, - 'pseudoboots': self.world.pseudoboots, - 'triforcegoal': self.world.treasure_hunt_count, - 'triforcepool': self.world.treasure_hunt_total, - 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} - } - def to_json(self): self.parse_data() out = OrderedDict() @@ -2513,22 +2526,29 @@ class Spoiler(object): return json.dumps(out) - def to_file(self, filename): + def mystery_meta_to_file(self, filename): + self.parse_meta() + with open(filename, 'w') as outfile: + outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) + 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('Logic: %s\n' % self.metadata['logic'][player]) + + def meta_to_file(self, filename): def yn(flag): return 'Yes' if flag else 'No' - self.parse_data() + line_width = 35 + self.parse_meta() with open(filename, 'w') as outfile: - outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) + outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Players: %d\n' % self.world.players) outfile.write('Teams: %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))) - if len(self.hashes) > 0: - for team in range(self.world.teams): - outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Mode: %s\n' % self.metadata['mode'][player]) @@ -2538,23 +2558,30 @@ class Spoiler(object): if self.metadata['goal'][player] in ['triforcehunt', 'trinity']: outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) + outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) + outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") + outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) - outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") + outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") + outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") + if self.metadata['goal'][player] != 'trinity': + outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) - outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") - addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' - outfile.write('Crystals required for GT: %s\n' % (str(self.metadata['gt_crystals'][player]) + addition)) - addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else '' - outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) - if self.metadata['goal'][player] != 'trinity': - outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) - outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) - outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) @@ -2564,10 +2591,62 @@ class Spoiler(object): outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") - outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") - outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") - outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") - outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") + + if self.startinventory: + outfile.write('Starting Inventory:'.ljust(line_width)) + outfile.write('\n'.ljust(line_width+1).join(self.startinventory) + '\n') + + def hashes_to_file(self, filename): + with open(filename, 'r') as infile: + contents = infile.readlines() + + def insert(lines, i, value): + lines.insert(i, value) + i += 1 + return i + + idx = 2 + if self.world.players > 1: + idx = insert(contents, idx, 'Hashes:') + for player in range(1, self.world.players + 1): + if self.world.players > 1: + idx = insert(contents, idx, f'\nPlayer {player}: {self.world.get_player_names(player)}\n') + if len(self.hashes) > 0: + for team in range(self.world.teams): + player_name = self.world.player_names[player][team] + label = f"Hash - {player_name} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ' + idx = insert(contents, idx, f'{label}{self.hashes[player, team]}\n') + if self.world.players > 1: + insert(contents, idx, '\n') # return value ignored here, if you want to add more lines + + with open(filename, "w") as f: + contents = "".join(contents) + f.write(contents) + + def to_file(self, filename): + self.parse_data() + with open(filename, 'a') as outfile: + line_width = 35 + + outfile.write('\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}: {medallion} Medallion\n') + 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 self.entrances: + # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly + outfile.write('\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: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2588,19 +2667,6 @@ class Spoiler(object): # 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()])) - if self.entrances: - # 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()])) - outfile.write('\n\nMedallions:\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion} Medallion') - outfile.write('\n\nBottle Refills:\n') - for fairy, bottle in self.bottles.items(): - outfile.write(f'\n{fairy}: {bottle}') - if self.startinventory: - outfile.write('\n\nStarting Inventory:\n\n') - outfile.write('\n'.join(self.startinventory)) # 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 @@ -2618,6 +2684,8 @@ class Spoiler(object): 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 playthrough_to_file(self, filename): + with open(filename, 'a') as outfile: # 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\nPlaythrough:\n\n') diff --git a/Main.py b/Main.py index 174a2568..967bb77f 100644 --- a/Main.py +++ b/Main.py @@ -130,6 +130,8 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') + outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' + for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -143,6 +145,13 @@ def main(args, seed=None, fish=None): if item: world.push_precollected(item) + if args.create_spoiler 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: + world.spoiler.mystery_meta_to_file(output_path(f'{outfilebase}_meta.txt')) + + for player in range(1, world.players + 1): if world.mode[player] != 'inverted': create_regions(world, player) else: @@ -258,8 +267,6 @@ def main(args, seed=None, fish=None): balance_money_progression(world) ensure_good_pots(world, True) - outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' - rom_names = [] jsonout = {} enemized = False @@ -334,6 +341,14 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) + if args.mystery: + world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) + elif args.create_spoiler and not args.jsonout: + world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli", "cli", "patching.spoiler")) + world.spoiler.to_file(output_path(f'{outfilebase}_Spoiler.txt')) + if not args.skip_playthrough: logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) @@ -346,7 +361,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: outfile.write(world.spoiler.to_json()) else: - world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + world.spoiler.playthrough_to_file(output_path(f'{outfilebase}_Spoiler.txt')) YES = world.fish.translate("cli","cli","yes") NO = world.fish.translate("cli","cli","no") diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 59aecc23..9d3ec198 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -38,6 +38,7 @@ "cannot.reach.required": "Not all required items reachable. Something went terribly wrong here.", "patching.rom": "Patching ROM", "patching.spoiler": "Creating Spoiler", + "create.meta": "Creating Meta Info", "calc.playthrough": "Calculating Playthrough", "made.rom": "Patched ROM: %s", "made.playthrough": "Printed Playthrough: %s", From f5307dacc12b6ff893b5fbfd5084ac4674952438 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 17 Jun 2022 15:12:37 -0600 Subject: [PATCH 190/293] Waterfall logic fix. Inverted seems to have been fixed years ago --- EntranceShuffle.py | 2 ++ Regions.py | 3 ++- Rules.py | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index d1bfdbb8..82aa6a75 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3066,6 +3066,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), ('Zoras River', 'Zoras River'), + ('Zora Waterfall Entryway', 'Zora Waterfall Entryway'), + ('Zora Waterfall Water Drop', 'Light World'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), ('Kings Grave Inner Rocks', 'Light World'), ('Kings Grave Mirror Spot', 'Kings Grave Area'), diff --git a/Regions.py b/Regions.py index 29d61bf9..c0ef0364 100644 --- a/Regions.py +++ b/Regions.py @@ -15,7 +15,7 @@ def create_regions(world, player): 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter', 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', - 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate', + 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Zora Waterfall Entryway', 'Hyrule Castle Main Gate', 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']), create_lw_region(player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), create_lw_region(player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), @@ -28,6 +28,7 @@ def create_regions(world, player): create_cave_region(player, 'Blinds Hideout (Top)', 'a bounty of five items', ["Blind's Hideout - Top"]), create_cave_region(player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), create_lw_region(player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), + create_lw_region(player, 'Zora Waterfall Entryway', None, ['Waterfall of Wishing', 'Zora Waterfall Water Drop']), create_cave_region(player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), diff --git a/Rules.py b/Rules.py index dbe0ede4..2a8ddb35 100644 --- a/Rules.py +++ b/Rules.py @@ -793,6 +793,9 @@ def default_rules(world, player): set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) # can be fake flippered into, but is in weird state inside that might prevent you from doing things. Can be improved in future Todo + # flippers or pearl to leave (via fake flippers) + set_rule(world.get_entrance('Zora Waterfall Water Drop', player), + lambda state: state.has('Flippers', player) or state.has_Pearl(player)) set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player)) # will get automatic moon pearl requirement set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player)) @@ -1032,6 +1035,8 @@ def inverted_rules(world, player): def no_glitches_rules(world, player): if world.mode[player] != 'inverted': add_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player)) + add_rule(world.get_entrance('Zora Waterfall Entryway', player), lambda state: state.has('Flippers', player)) + add_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) add_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to add_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) add_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) From 377989d40691cc9ad4f3a5167b45506aaf555fc7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 22 Jun 2022 10:43:39 -0600 Subject: [PATCH 191/293] Update release notes and version --- Main.py | 2 +- RELEASENOTES.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 967bb77f..1d060c30 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.2.5-v' +__version__ = '1.0.2.6-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a27c05d0..e1a1b3cb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -157,6 +157,11 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile +* 1.0.2.6 + * Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal. + * Added a check for package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea. + * Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work. + * Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (or moon pearl in glitched modes that allow minor glitches in logic) * 1.0.2.5 * Some textual changes for hints (capitalization standardization) * Item will be highlighted in red if experimental is on From fe814005d6c5b730f07337d02c008f67dfecd195 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 23 Jun 2022 10:36:51 -0600 Subject: [PATCH 192/293] Waterfall of Wishing logic exit fix revised --- Main.py | 2 +- RELEASENOTES.md | 2 ++ Rules.py | 13 +++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Main.py b/Main.py index 1d060c30..7d2375ac 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.2.6-v' +__version__ = '1.0.2.7-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e1a1b3cb..ab1c6ef0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -157,6 +157,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile +* 1.0.2.7 + * Revised: Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well) * 1.0.2.6 * Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal. * Added a check for package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea. diff --git a/Rules.py b/Rules.py index 2a8ddb35..c7b9ef44 100644 --- a/Rules.py +++ b/Rules.py @@ -792,10 +792,10 @@ def default_rules(world, player): set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) # can be fake flippered into, but is in weird state inside that might prevent you from doing things. Can be improved in future Todo - # flippers or pearl to leave (via fake flippers) - set_rule(world.get_entrance('Zora Waterfall Water Drop', player), - lambda state: state.has('Flippers', player) or state.has_Pearl(player)) + # can be fake flippered into, but is in weird state inside that might prevent you from doing things. + set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) + # to leave via fake flippers, you'd need pearl and have waterwalk or swimming state, so just require flippers + set_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player)) # will get automatic moon pearl requirement set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player)) @@ -1032,11 +1032,12 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Inverted Ganons Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) + def no_glitches_rules(world, player): if world.mode[player] != 'inverted': add_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player)) - add_rule(world.get_entrance('Zora Waterfall Entryway', player), lambda state: state.has('Flippers', player)) - add_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) + set_rule(world.get_entrance('Zora Waterfall Entryway', player), lambda state: state.has('Flippers', player)) + set_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) add_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to add_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) add_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) From d488df6d18197a2a14889a21b3f42109ff49e8d5 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 29 Jun 2022 12:58:10 -0600 Subject: [PATCH 193/293] Pot substitutions for certain items --- Main.py | 2 +- RELEASENOTES.md | 2 ++ Rom.py | 2 +- data/base2current.bps | Bin 93041 -> 93142 bytes 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 7d2375ac..e4fe1ae1 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.2.7-v' +__version__ = '1.0.2.8-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ab1c6ef0..36357d28 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -157,6 +157,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile +* 1.0.2.8 + * Pot substitution for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can results from a tree pull. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other nomral pot contents. * 1.0.2.7 * Revised: Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well) * 1.0.2.6 diff --git a/Rom.py b/Rom.py index b7015162..ccf06ee7 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'e33204b6023f07025eba16874308f57d' +RANDOMIZERBASEHASH = 'c7ea39c6e81d5ba010752de102484aef' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index fc22d9c3956780075f6752188bd52fca2ae6c189..592ad7425896bc5aaa8508e40a0929706d874ea8 100644 GIT binary patch delta 466 zcmV;@0WJRV)&WGO$v6CAI zegUbIng@_N{jR9$>X?-eu&96m>O6>*A_=jmum_hZGJ&NDB2ciX8X}OGC!m!gSg)w+ z>J)*EA|}+JPz`{QBFd9w2s#gquBhsCfQbUipj?27AF;Eo2uA@4ezB=R!uBaS>jUrF3gJTT0V+;XO5DML{sD%oS@X;Wnm%khVR5Vq9jUWe(A_wj!2dJqC z0Khf?&;_X$0Dv68L;%nQsTTl%62K?`&;_X&0DuUVP9|1K1;g@kA0XhNU zm#iNF-vT7EmyI9+K@8clfD*HWvBDs!iL#g4AOSW45Ggt%4ey^y20qQ)6l_GessIUi@DKdej2_nC*s2U=Wm?xl>BFC<%>gp7MjUp!0 zpim8fks>*hYX~|I{H>_!bbyHh%Aj0;iH@+dvbQ*}{;;zI3S9yTnysiD zf{h}+t%GU|w`vRlQV Date: Wed, 29 Jun 2022 13:01:34 -0600 Subject: [PATCH 194/293] Spelling and grammar --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 36357d28..c56aa3fc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -158,7 +158,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile * 1.0.2.8 - * Pot substitution for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can results from a tree pull. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other nomral pot contents. + * Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents. * 1.0.2.7 * Revised: Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well) * 1.0.2.6 From 04fc0b442dc7652a4b355425ab44fffbd7be6d58 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 30 Jun 2022 11:51:34 -0600 Subject: [PATCH 195/293] Hotfix for enemizer and pot substitutions --- Main.py | 2 +- RELEASENOTES.md | 2 ++ Rom.py | 2 +- data/base2current.bps | Bin 93142 -> 93161 bytes 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index e4fe1ae1..7218e202 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.2.8-v' +__version__ = '1.0.2.9-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c56aa3fc..3b52ca83 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -157,6 +157,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile +* 1.0.2.9 + * Hotfix for enemizer and the new substitution * 1.0.2.8 * Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents. * 1.0.2.7 diff --git a/Rom.py b/Rom.py index ccf06ee7..c96a5c71 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'c7ea39c6e81d5ba010752de102484aef' +RANDOMIZERBASEHASH = 'd29420ac98a70a5f539b730d197bff29' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 592ad7425896bc5aaa8508e40a0929706d874ea8..a06883144bc1eaea09656d8f2bc397343cb969d5 100644 GIT binary patch delta 430 zcmV;f0a5O6>*BIdBDum_hZGJ&NDA}z3}8X}OGC!m!gHLs}Z z>J)*EA|}+JPz`{QBBzsU2s#yNuBhsCfQbUipj?27{IIBKvz{Pav$_aD0SJ$=sOq?l zBA~Ma3SI&T2(G9cf{h|9gJ}%6X$%2V5COWEy&M6z1|fmsF#2GZogD#20SK4m9RXqi z8J9;M0bUPeeS?7+tAdTMA_sn(e}0lt^k9|1Z6zLyao0X9A%flv=?zy#0%YiMY|7O({L zf`V)GiGpj};@#fB7O(+esR6(kkOHX%g$*FVt&2;mxgD{qU?4ZKB3f0T;KjApsmp2yXIxKW{x(b+9F^#{d8T delta 411 zcmV;M0c8H^*9F$s1(1pX+mVaU1RD(G^OIfyE&-XdjRA=T3oMqAA`Gsm>WGO$lOP9w z0jZOq2ar1buBhtjn3WH(sDJ_LJcyMd39+cK2bU=_fu#u|P_U>PB9NFTpp_z6uc+$k z6oHK*Ce)x%4SbQ*} zkFx{{UIGX!uBaS>jUrEjY7Dn(3;|OR0o|9r909i}ErQ}O`e57Q-QK_!un2?=x$}W* zsR6(kkOHX%g$*FVt&2;mx$>>6YnOW+0Y(8%m%<$ZVgX;5B_07@4=R3xff=iUjjtjH zew#&ymy#X&6o6bh(gtfHF0!b{CsvnB9|1A}@0Wfb z0W|^Pm$V-NIsxIA?;im+358G(moF~{fU%cjAOVFO95?GNfZ{OvYvK^XAg*y9@IRb! z9<6X53aY;(mjodJLJQflfD*HWvBDs!iI-6!0XG2 Date: Thu, 7 Jul 2022 14:20:41 -0600 Subject: [PATCH 196/293] Merged in Volatile work --- BaseClasses.py | 2 +- CLI.py | 1 + Main.py | 6 +- Mystery.py | 14 +- RELEASENOTES.md | 239 +++++++++++++------------------- Rom.py | 2 +- data/base2current.bps | Bin 93161 -> 93156 bytes mystery_testsuite.yml | 32 +++-- resources/app/cli/args.json | 4 + source/test/MysteryTestSuite.py | 2 +- 10 files changed, 136 insertions(+), 166 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 12b5665b..7f6a00c6 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2529,7 +2529,7 @@ class Spoiler(object): def mystery_meta_to_file(self, filename): self.parse_meta() with open(filename, 'w') as outfile: - outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) + outfile.write(f'ALttP Dungeon Randomizer Version {self.metadata["version"]}\n\n') 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))) diff --git a/CLI.py b/CLI.py index 53fda3ad..ba5090ec 100644 --- a/CLI.py +++ b/CLI.py @@ -146,6 +146,7 @@ def parse_settings(): "accessibility": "items", "algorithm": "balanced", 'mystery': False, + 'suppress_meta': False, "restrict_boss_items": "none", # Shuffle Ganon defaults to TRUE diff --git a/Main.py b/Main.py index 7218e202..7aba1276 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.2.9-v' +__version__ = '1.0.1.0-u' from source.classes.BabelFish import BabelFish @@ -148,7 +148,7 @@ def main(args, seed=None, fish=None): if args.create_spoiler 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: + if args.mystery and not args.suppress_meta: world.spoiler.mystery_meta_to_file(output_path(f'{outfilebase}_meta.txt')) for player in range(1, world.players + 1): @@ -341,7 +341,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) - if args.mystery: + if args.mystery and not args.suppress_meta: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) elif args.create_spoiler and not args.jsonout: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) diff --git a/Mystery.py b/Mystery.py index ce58a8e2..20e0e2ca 100644 --- a/Mystery.py +++ b/Mystery.py @@ -29,6 +29,7 @@ def main(): parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) parser.add_argument('--create_spoiler', 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') parser.add_argument('--rom') parser.add_argument('--enemizercli') @@ -64,6 +65,7 @@ def main(): erargs.names = args.names erargs.create_spoiler = args.create_spoiler erargs.suppress_rom = args.suppress_rom + erargs.suppress_meta = args.suppress_meta erargs.bps = args.bps erargs.race = True erargs.outputname = seedname @@ -201,12 +203,14 @@ def roll_settings(weights): ret.crystals_ganon = get_choice('ganon_open') - goal_min = get_choice_default('triforce_goal_min', default=20) - goal_max = get_choice_default('triforce_goal_max', default=20) - pool_min = get_choice_default('triforce_pool_min', default=30) - pool_max = get_choice_default('triforce_pool_max', default=30) + from ItemList import set_default_triforce + default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0) + goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal) + goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal) + pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool) + pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool) ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) - min_diff = get_choice_default('triforce_min_difference', default=10) + min_diff = get_choice_default('triforce_min_difference', default=default_tf_pool-default_tf_goal) ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) ret.mode = get_choice('world_state') diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b52ca83..65293af1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,7 +25,7 @@ Note for multiworld: due to the design of the pottery lottery, only 256 items fo ### Colorize Pots -If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exception "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. +If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exceptions include "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. Note that Mystery will colorize all pots if lottery is chosen randomly. CLI `--colorizepots` @@ -39,15 +39,40 @@ CLI `--dropshuffle` "Drop and Pot Keys" or `--keydropshuffle` is still availabe for use. This simply sets the pottery to keys and turns dropshuffle on as well to have the same behavior as the old setting. -The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or `--shufflepots` and works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches while having every pot in the pool. +The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or `--shufflepots` and works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode. #### Tracking Notes -The sram locations for pots and sprite drops are not yet final, please reach out for assistance or investigate the rom changes. +The sram locations for pots and sprite drops are now final, please reach out for assistance or investigate the rom changes if needed. + +## New Options + +### Collection Rate + +You can set the collection rate counter on using the "Display Collection Rate" on the Game Options tab are using the CLI option `--collection_rate`. Mystery seeds will not display the total. + +### Goal: Trinity + +Triforces are placed behind Ganon, on the pedestal, and on Murahdahla with 8/10 triforce pieces required. Recommend to run with 4-5 Crystal requirement for Ganon. Automatically pre-opens the pyramid. + +### Boss Shuffle: Unique + +At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything. + +### MSU Resume + +Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option. + +### BPS Patch + +Creates a bps patch for the seed. Found on the "Generation Setup" tab called "Create BPS Patches" or `--bps`. Can turn off generating a rom using the existing "Create Patched ROM" option or `--suppress_rom`. There is an option on the Adjust/Patch tab to select a bps file to apply to the Base Rom selected on the Generation Setup tab using the Patch Rom button. Selected adjustments will be applied during patching. + +## New Font + +Font updated to support lowercase English. Lowercase vs. uppercase typos may exist. Note, you can use lowercase English letters on the file name. ## Restricted Item Placement Algorithm - The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed. When referenced below, Major Items include all Y items, all A items, all equipment (swords, shields, & armor) and Heart Containers. Dungeon items are considered major if shuffled outside of dungeons. Bomb and arrows upgrades are Major if shopsanity is turned on. The arrow quiver and universal small keys are Major if retro is turned on. Triforce Pieces are Major if that is the goal, and the Bomb Bag is Major if that is enabled. @@ -74,12 +99,12 @@ The fill attempts to place all major items in dungeons. It will overflow to the ### District Restriction -The world is divided up into different regions or districts. Each dungeon is it's own district. The overworld consists of the following districts: +The world is divided up into different regions or districts. Each dungeon is its own district. The overworld consists of the following districts: Light world: * Kakariko (The main screen, blacksmith screen, and library/maze race screens) -* Northwest Hyrule (The lost woods and fortune teller all the way to the rive west of the potion shop) +* Northwest Hyrule (The lost woods and fortune teller screens all the way to the river west of the potion shop) * Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove) * Desert (From the thief to the main desert screen) * Lake Hylia (Around the lake) @@ -108,10 +133,9 @@ In multiworld, the districts chosen apply to all players. ## New Hints -Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it check for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish. At least two "foolish" districts are chosen and the rest are random. +Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish. - -### Overworld Map shows dungeon location +## Overworld Map shows Dungeon Entrances Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower. @@ -147,7 +171,9 @@ As before, the boss may have any item including any dungeon item that could occu ##### mapcompass -The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss. +~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~ + +Currently bugged, not recommended for use. ##### dungeon @@ -155,145 +181,70 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o ## Notes and Bug Fixes -#### Volatile - -* 1.0.2.9 - * Hotfix for enemizer and the new substitution -* 1.0.2.8 - * Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents. -* 1.0.2.7 - * Revised: Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well) -* 1.0.2.6 - * Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal. - * Added a check for package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea. - * Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work. - * Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (or moon pearl in glitched modes that allow minor glitches in logic) -* 1.0.2.5 - * Some textual changes for hints (capitalization standardization) - * Item will be highlighted in red if experimental is on - * Bug with 0 GT crystals not opening GT - * Settings code fix - * Fix for pottery not counting items in certain caves that share a supertile with shops -* 1.0.2.4 - * Updated tourney winners (included Doors Async League winners) - * Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking -* 1.0.2.3 - * Fix MultiClient for new shop data location in SRAM - * Some minor text updates -* 1.0.2.2 - * Change to all key pots and enemy key drops: always use the same address - * Don't colorize key pots in mystery if the item is "forced" -* 1.0.2.1 - * Fix for paired doors - * Fix for forbidding certain dashable doors (it actually does something this time) -* 1.0.2.0 - * Updated baserom to bleeding edge - * Pottery and enemy SRAM re-located to final destination - * Bulk of work on new font - * Updated TFH to support up to 850 pieces - * Fix for major item algorithm and pottery - * Updated map display on keysanity menu to work better with overworld_amp option - * Minor bug in crossed doors - * Minor bug in MultiClient which would count switches -* 1.0.1.13 - * New pottery modes - * Trinity goal added - * Potential fix for pottery hera key - * Fix for arrows sneaking into item pool with rupee bow - * Fixed msu resume bug on patcher - * Bonk Recoil OHKO fix (again) -* 1.0.1.12 - * Fix for Multiworld forfeits, shops and pot items now included - * Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill. - * MultiServer fix for ssl certs and python - * Inverted bug - * Fix for hammerdashing pots, if sprite limit is reached, items won't spawn, but error beep won't play either because of other SFX - * Arrghus splash no longer used for pottery sprites (used apple instead) - * Killing enemies via freeze + hammer properly results in the droppable item instead of the freeze prize - * Forbid certain doors from being dashable when you either can't dash them open (but bombs would work) or you'd fall into a pit from the bonk recoil in OHKO - * Logic refinements - * Skull X Room requires Boots or access to Skull Back Drop - * GT Falling Torches requires Boots to get over the falling tile gap (this is a stop-gap measure until more sophisticated crystal switch traversal is possible) - * Fixed a couple rain state issues -* 1.0.1.11 - * Separated Collection Rate counter from experimental - * Added MSU Resume option - * Ensured pots in TR Dark Ride need lamp - * Fix for GT Crystal Conveyor not requiring Somaria/Bombs to get through - * Fixes for Links House being at certain entrances (did not generate) -* 1.0.1.10 - * More location count fixes - * Add major_only algorithm to code - * Include 1.0.0.2 fixes -* 1.0.1.9 - * Every pot you pick up that wasn't part of the location pool does not count toward the location count - * Fix for items spawning where a thrown pot was - * Fix for vanilla_fill, it now prioritizes heart container placements - * Fix for dungeon counter showing up in AT/HC in crossed dungeon mode - * Fix for TR Dark Ride (again) and some ohko rules refinement -* 1.0.1.8 - * Every pot you pick up now counts toward the location count - * A pot will de-spawn before the item under it does, error beep only plays if it still can't spawn - * Updated item counter & credits to support 4 digits - * Updated compass counter to support 3 digits (up to 255) - * Updated retro take-anys to not replace pot locations when pottery options are used - * Updated mystery_example.yml - * Fixed usestartinventory with mystery - * Fixed a bug with the old pot shuffle (crashed when used) -* 1.0.1.7 - * Expanded Mystery logic options (e.g. owglitches) - * Allowed Mystery.py to create BPS patches - * Allow creation of BPS and SFC files (no longer mutually exclusive) - * Pedestal goal + vanilla swords places a random sword in the pool - * Rebalanced trash ditching algo for seeds with lots of triforce pieces - * Added a few more places Links House shouldn't go when shuffled - * Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count - * Fixed usestartinventory flag (can be use on a per player basis) - * Fix for map indicators on keysanity menu not showing up - * Potential sprite selector fix for systems with SSL issues -* 1.0.1.6 - * A couple new options for lighter pottery modes (Cave Pots and Dungeon Pots) - * New option for Boss Shuffle: Unique (Prize bosses will be one of each, but GT bosses can be anything) - * Support for BPS patch creation and applying patches during adjustment - * Fix for SFX shuffle - * Fix for Standard ER where locations in rain state could be in logic - * Fix for Ice Refill room pots, require being able to hit a switch for bombbag mode -* 1.0.1.5 - * Fix for Hera Basement Cage item inheriting last pot checked - * Update indicators on keysanity menu for overworld map option -* 1.0.1.4 - * Reverted SRAM change (the underlying refactor isn't done yet) -* 1.0.1.3 - * Fixed inverted generation issues with pottery option - * Moved SRAM according to SRAM standard - * Removed equitable algorithm - * Upped TFH goal limit to 254 - * Cuccos should no longer cause trap door rooms to not open - * Added double click fix for install.py - * Fix for pottery item palettes near bonkable torches - * Fix for multiworld progression balancing would place Nothing or Arrow items -* 1.0.1.2 - * Fixed logic for pots in TR Hub and TR Dark Ride - * Fix for districting + shopsanity - * Hint typo correction -* 1.0.1.1 - * Fixed logic for pots in the Ice Hammer Block room (Glove + Hammer required) - * Fixed logic for 2 pots in the Ice Antechamber (Glove required) - * Fixed retro not saving keys when grabbed from under pots in caves - * Fixed GUI not applying Drop shuffle when "Pot and Drops" are marked - * Fixed dungeon counts when one of Pottery or Drops are disabled - #### Unstable +* 1.0.1.0 + * Large features + * New pottery modes - see notes above + * Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents. + * Updated TFH to support up to 850 pieces + * New font support + * Trinity goal added + * Separated Collection Rate counter from experimental + * Added MSU Resume option + * Support for BPS patch creation and applying patches during adjustment + * New option for Boss Shuffle: Unique (Prize bosses will be one of each, but GT bosses can be anything) + * Logic Notes + * Skull X Room requires Boots or access to Skull Back Drop + * GT Falling Torches requires Boots to get over the falling tile gap (this is a stop-gap measure until more sophisticated crystal switch traversal is possible) + * Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well) + * Fix for GT Crystal Conveyor not requiring Somaria/Bombs to get through + * Pedestal goal + vanilla swords places a random sword in the pool + * Added a few more places Links House shouldn't go when shuffled + * Small features + * Added a check for python package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea. + * Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work. + * Updated tourney winners (included Doors Async League winners) + * Some textual changes for hints (capitalization standardization) + * Item will be highlighted in red if experimental is on. This will likely be removed. + * Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill. + * Expanded Mystery logic options (e.g. owglitches) + * Updated indicators on keysanity menu for overworld map option + * Bug fixes: + * Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal. + * Bug with 0 GT crystals not opening GT + * Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking + * Settings code fix + * Fix for forbidding certain dashable doors (it actually does something this time) + * Fix for major item algorithm and pottery + * Updated map display on keysanity menu to work better with overworld_map option + * Minor bug in crossed doors + * Fix for Multiworld forfeits, shops and pot items now included + * MultiServer fix for ssl certs and python + * forbid certain doors from being dashable when you either can't dash them open (but bombs would work) or you'd fall into a pit from the bonk recoil in OHKO + * Fixed a couple rain state issues + * Add major_only algorithm to settings code + * Fixes for Links House being at certain entrances (did not generate) + * Fix for vanilla_fill, it now prioritizes heart container placements + * Fix for dungeon counter showing up in AT/HC in crossed dungeon mode + * Fixed usestartinventory with mystery + * Added double click fix for install.py + * Fix for SFX shuffle + * Fix for districting + shopsanity + * Fix for multiworld progression balancing would place Nothing or Arrow items + * Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count + * Fixed usestartinventory flag (can be use on a per player basis) + * Sprite selector fix for systems with SSL issues + * Fix for Standard ER where locations in rain state could be in logic * 1.0.0.3 - * overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown) - * Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix) - * Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks) + * overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown) + * Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix) + * Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks) * 1.0.0.2 - * Include 1.0.1 fixes - * District hint rework + * Include 1.0.1 fixes + * District hint rework * 1.0.0.1 - * Add Light Hype Fairy to bombbag mode as needing bombs + * Add Light Hype Fairy to bombbag mode as needing bombs ### From stable DoorDev diff --git a/Rom.py b/Rom.py index c96a5c71..7861a5cd 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'd29420ac98a70a5f539b730d197bff29' +RANDOMIZERBASEHASH = '9008f4335101689f01184e58295fdbc5' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index a06883144bc1eaea09656d8f2bc397343cb969d5..464ceaf6105a2637981d2d107a868a08a143c8e4 100644 GIT binary patch delta 8913 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 IMS)20f42OD@&Et; delta 8889 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 Date: Fri, 8 Jul 2022 12:13:34 -0500 Subject: [PATCH 197/293] Adapted changes to pre-set overworld flags to new initial sram system --- BaseClasses.py | 2 -- InitialSram.py | 3 +++ Rom.py | 8 ++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 78aea401..a4919149 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -63,7 +63,6 @@ class World(object): self.lock_aga_door_in_escape = False self.save_and_quit_from_boss = True self.accessibility = accessibility.copy() - self.initial_overworld_flags = {} self.fix_skullwoods_exit = {} self.fix_palaceofdarkness_exit = {} self.fix_trock_exit = {} @@ -122,7 +121,6 @@ class World(object): set_player_attr('ganon_at_pyramid', True) set_player_attr('ganonstower_vanilla', True) set_player_attr('sewer_light_cone', self.mode[player] == 'standard') - set_player_attr('initial_overworld_flags', [0] * 0x80) set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.is_tile_swapped(0x05, player)) set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) diff --git a/InitialSram.py b/InitialSram.py index 772e1d46..3de0dffe 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -53,6 +53,9 @@ class InitialSram: def pre_open_pyramid_hole(self): self._or_value(OVERWORLD_DATA+0x5B, 0x20) + def pre_set_overworld_flag(self, owid, bitmask) + self._or_value(OVERWORLD_DATA+owid, bitmask) + def set_starting_equipment(self, world: object, player: int): equip = [0] * (0x340 + 0x4F) equip[0x36C] = 0x18 diff --git a/Rom.py b/Rom.py index 57b4db8e..add74a53 100644 --- a/Rom.py +++ b/Rom.py @@ -1403,10 +1403,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00) rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles - # starting overworld flags - assert len(world.initial_overworld_flags[player]) == 0x80 and all([i < 0x100 for i in world.initial_overworld_flags[player]]) - rom.write_bytes(0x183280, world.initial_overworld_flags[player]) - # Starting equipment if world.pseudoboots[player]: rom.write_byte(0x18008E, 0x01) @@ -2765,7 +2761,7 @@ def patch_shuffled_dark_sanc(world, rom, player): dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) 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[dark_sanc_entrance][1] door_index = door_addresses[str(dark_sanc_entrance)][0] - world.initial_overworld_flags[player][ow_area] |= door_addresses[dark_sanc_entrance][2] + rom.initial_sram.pre_set_overworld_flag(ow_area, door_addresses[dark_sanc_entrance][2]) rom.write_byte(0x180241, 0x01) rom.write_byte(0x180248, door_index + 1) @@ -2780,7 +2776,7 @@ def patch_shuffled_bomb_shop(world, rom, player): bomb_shop_entrance = str([i for i in bomb_shop.entrances if i.parent_region.name != 'Menu'][0].name) 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[bomb_shop_entrance][1] door_index = door_addresses[str(bomb_shop_entrance)][0] - world.initial_overworld_flags[player][ow_area] |= door_addresses[bomb_shop_entrance][2] + rom.initial_sram.pre_set_overworld_flag(ow_area, door_addresses[bomb_shop_entrance][2]) rom.write_byte(0x180240, 0x02) rom.write_byte(0x180247, door_index + 1) From 92bc88bd41c46260d5a5e799cdfe3781c59f8d6b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 8 Jul 2022 12:25:54 -0500 Subject: [PATCH 198/293] Adapted changes to pre-set overworld flags to new initial sram system --- InitialSram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InitialSram.py b/InitialSram.py index 3de0dffe..0be80de2 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -53,7 +53,7 @@ class InitialSram: def pre_open_pyramid_hole(self): self._or_value(OVERWORLD_DATA+0x5B, 0x20) - def pre_set_overworld_flag(self, owid, bitmask) + def pre_set_overworld_flag(self, owid, bitmask): self._or_value(OVERWORLD_DATA+owid, bitmask) def set_starting_equipment(self, world: object, player: int): From 673ac9ac239e5bf33902f96603b609796893a749 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 8 Jul 2022 16:17:10 -0500 Subject: [PATCH 199/293] Reverted some code to fix Trinity --- Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index c5b52332..73e572b9 100644 --- a/Rules.py +++ b/Rules.py @@ -59,7 +59,8 @@ def set_rules(world, player): # 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']: - add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) + if ('Murahdahla', player) in world._location_cache: + add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) # if swamp and dam have not been moved we require mirror for swamp palace if not world.swamp_patch_required[player]: From 51535d7a1020f15436b6c70e876566e9862d361e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 8 Jul 2022 22:12:59 -0500 Subject: [PATCH 200/293] Added Zelda Delivered rule to Flute usage if Standard --- BaseClasses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index a4919149..5073d6cd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1365,6 +1365,8 @@ class CollectionState(object): return self.has('Fire Rod', player) or self.has('Lamp', player) def can_flute(self, player): + if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player): + return False if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): return True lw = self.world.get_region('Kakariko Area', player) From 10caf09492fdf1bfd46924f8e1e75d2d8265beaa Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 9 Jul 2022 10:34:11 -0500 Subject: [PATCH 201/293] Fixed issue with filename always outputting DR, not OR --- Main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Main.py b/Main.py index c0ab72e7..b008593e 100644 --- a/Main.py +++ b/Main.py @@ -143,8 +143,6 @@ def main(args, seed=None, fish=None): else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' - outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' - for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] From 91f7bc8cefd146d04c3ef3802eb0d896c1ad5e88 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 11 Jul 2022 17:40:08 -0500 Subject: [PATCH 202/293] Suppress playthru for nologic --- Main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Main.py b/Main.py index b008593e..2fcfe9af 100644 --- a/Main.py +++ b/Main.py @@ -377,7 +377,7 @@ def main(args, seed=None, fish=None): if args.jsonout: with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: outfile.write(world.spoiler.to_json()) - else: + elif world.players > 1 or world.logic[1] != "nologic": world.spoiler.playthrough_to_file(output_path(f'{outfilebase}_Spoiler.txt')) YES = world.fish.translate("cli","cli","yes") @@ -697,9 +697,14 @@ def create_playthrough(world): old_world.spoiler.paths = dict() for player in range(1, world.players + 1): - old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) + if world.logic[player] != 'nologic': + old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) + # for path in dict(old_world.spoiler.paths).values(): + # if any(exit == 'Pyramid Fairy' for (_, exit) in path): + # old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) # we can finally output our playthrough old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} for i, sphere in enumerate(collection_spheres): - old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} + if world.logic[player] != 'nologic': + old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} From ae271b0e733643c19be4da9b5ac71fd76b47fd76 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 11 Jul 2022 17:44:18 -0500 Subject: [PATCH 203/293] Removing some permament negative effects of using copy_world --- EntranceShuffle.py | 9 +++------ Main.py | 13 +++++++++---- OverworldShuffle.py | 9 +++------ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index af2755ab..dc48acdc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1448,9 +1448,8 @@ def junk_fill_inaccessible(world, player): for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world) + base_world = copy_world(world, True) base_world.override_bomb_check = True - world.key_logic = {} # remove regions that have a dungeon entrance accessible_regions = list() @@ -1617,9 +1616,8 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world) + base_world = copy_world(world, True) base_world.override_bomb_check = True - world.key_logic = {} connect_simple(base_world, 'Links House S&Q', start_region, player) blank_state = CollectionState(base_world) @@ -1727,9 +1725,8 @@ def can_reach(world, entrance_name, region_name, player): for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world) + base_world = copy_world(world, True) base_world.override_bomb_check = True - world.key_logic = {} entrance = world.get_entrance(entrance_name, player) connect_simple(base_world, 'Links House S&Q', entrance.parent_region.name, player) diff --git a/Main.py b/Main.py index 2fcfe9af..7f5a7ed4 100644 --- a/Main.py +++ b/Main.py @@ -398,7 +398,7 @@ def main(args, seed=None, fish=None): return world -def copy_world(world): +def copy_world(world, partial_copy=False): # 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, @@ -544,9 +544,10 @@ def copy_world(world): ret.dungeon_layouts = world.dungeon_layouts ret.key_logic = world.key_logic ret.dungeon_portals = world.dungeon_portals - for player, portals in world.dungeon_portals.items(): - for portal in portals: - connect_portal(portal, ret, player) + if not partial_copy: + for player, portals in world.dungeon_portals.items(): + for portal in portals: + connect_portal(portal, ret, player) ret.sanc_portal = world.sanc_portal from OverworldShuffle import categorize_world_regions @@ -554,6 +555,10 @@ def copy_world(world): categorize_world_regions(ret, player) set_rules(ret, player) + if partial_copy: + # undo some of the things that unintentionally affect the original world object + world.key_logic = {} + return ret diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 0c0a2758..0ccca931 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -882,8 +882,7 @@ def build_sectors(world, player): # perform accessibility check on duplicate world for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world) - world.key_logic = {} + base_world = copy_world(world, True) # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) regions = list(OWTileRegions.copy().keys()) @@ -958,9 +957,8 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F if build_copy_world: for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world) + base_world = copy_world(world, True) base_world.override_bomb_check = True - world.key_logic = {} else: base_world = world @@ -1048,8 +1046,7 @@ def validate_layout(world, player): for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world) - world.key_logic = {} + base_world = copy_world(world, True) explored_regions = list() if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]: From 426d5d3e8430253fd792c7cfa5bee3a737e9b476 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 11 Jul 2022 17:45:09 -0500 Subject: [PATCH 204/293] Fixed beatable only check in OWR validation --- OverworldShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 0ccca931..347fc292 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -972,7 +972,7 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F return explored_regions def validate_layout(world, player): - if world.accessibility[player] == 'beatable': + if world.accessibility[player] == 'none': return True entrance_connectors = { From 359b283b948748f8e204a75e35e58f54d34fe5b0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 11 Jul 2022 17:47:16 -0500 Subject: [PATCH 205/293] Suppress playthru for nologic --- Main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Main.py b/Main.py index 7f5a7ed4..ac5843e5 100644 --- a/Main.py +++ b/Main.py @@ -704,9 +704,6 @@ def create_playthrough(world): for player in range(1, world.players + 1): if world.logic[player] != 'nologic': old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - # for path in dict(old_world.spoiler.paths).values(): - # if any(exit == 'Pyramid Fairy' for (_, exit) in path): - # old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) # we can finally output our playthrough old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} From 5e0ec96c66787c8b056eb2b71e6c3c5d657fdf27 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 12 Jul 2022 01:32:16 -0500 Subject: [PATCH 206/293] Fixed issue with playthrus for Murahdahla+Beatable --- Rules.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Rules.py b/Rules.py index 73e572b9..35572ec5 100644 --- a/Rules.py +++ b/Rules.py @@ -59,8 +59,9 @@ def set_rules(world, player): # 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 ('Murahdahla', player) in world._location_cache: - add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[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])) # if swamp and dam have not been moved we require mirror for swamp palace if not world.swamp_patch_required[player]: From 77451e419411d655ac2a9da3f99d465173a89b19 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 12 Jul 2022 02:01:29 -0500 Subject: [PATCH 207/293] Version bump 0.2.8.0 --- CHANGELOG.md | 53 +++++++++++++++++++++++++-------------------- OverworldShuffle.py | 2 +- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb65f37..aef17765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,15 @@ # Changelog +### 0.2.8.0 +- ~Merged DR v1.0.1.0 - Pottery options, BPS support, MSU Resume, Collection Rate Counter~ +- Various improvements to increase generation success rate and reduce generation time +- Fixed issue with playthru recognizing Aga accessibility +- Fixed issue with applying rules correctly to Murahdahla, fixing Murahdahla+Beatable issues +- Fixed issue with Flute+Rainstate, flute use is no longer in logic until Zelda is delivered + ### 0.2.7.3 - Restructured OWR algorithm to include some additional scenarios not previously allowed -- Added new Inverted D-pad controls for Social Distorion (ie. Mirror Mode) support +- Added new Inverted D-pad controls for Social Distortion (ie. Mirror Mode) support - Crossed OWR/Special OW Areas are now included in the spoiler log - Fixed default TF pieces with Trinity in Mystery - Added bush crabs to rupee farm logic (only in non-enemizer) @@ -19,7 +26,7 @@ - Added proper branch-specific versioning (ie. Dev branch has '-u' suffixing the version number while Release/Main branch does not) ### 0.2.7.0 -- ~~Merged DR v1.0.0.3 - MANY changes, major things listed below~~ +- ~Merged DR v1.0.0.3 - MANY changes, major things listed below~ - New Item Fills (Districts/Vanilla/Major Location/Dungeon) - New OW Map Prize Indicators (In ER, map checks can spoil dungeon locations with a user setting) - Forbidden Boss Items (Exclude certain dungeon items from dropping on bosses) @@ -70,7 +77,7 @@ - Fixed issue with incorrect Mirror bonking - Fixed issue with old man follower death to Pyramid - Fixed Hera boss music not playing when boss not defeated -- ~~Merged DR v0.5.1.7 - TT boss trap door fix/Applied Glitched flag~~ +- ~Merged DR v0.5.1.7 - TT boss trap door fix/Applied Glitched flag~ ### 0.2.4.0 - Added Guaranteed OWR Reachability @@ -96,7 +103,7 @@ - Fake flipper damage fix improved to skip the long delay after the scroll - Fixed missing Blue Potion in Lake Shop in Inverted - Added legacy OW Crossed option 'None (Allowed)' to support old behavior when invalid option was used in Mystery -- ~~Merged DR v0.5.1.6 - Money balancing fix/Boss logic fixes with Bombbag~~ +- ~Merged DR v0.5.1.6 - Money balancing fix/Boss logic fixes with Bombbag~ ### 0.2.3.3 - Added OW Layout validation that reduces the cases where some screens are unreachable @@ -138,7 +145,7 @@ - Fixed music track change to Sanc music when Standard mode is delivering Zelda - Fixed SP flooding issue - Fixed issue with Shuffle Ganon in CLI/GUI -- ~~Merged DR v0.5.1.5 - Mystery subweights~~ +- ~Merged DR v0.5.1.5 - Mystery subweights~ ### 0.2.1.2 - Fixed issue with whirlpools not changing world when in Crossed OW @@ -159,7 +166,7 @@ - Smith deletion on S+Q only occurs if Blacksmith not reachable from starting locations - Spoiler log improvements to prevent spoiling in the beginning 'meta' section - Various minor fixes and improvements -- ~~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~~ +- ~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~ ### 0.1.9.4 - Hotfix for bad 0.1.9.3 version @@ -172,11 +179,11 @@ ### 0.1.9.2 - Fixed spoiler log and mystery for new Crossed/Mixed structure - Minor preparations and tweaks to ER framework (added global Entrance/Exit pool) -- ~~Merged DR v0.5.1.2 - Blind Prison shuffled outside TT/Keylogic Improvements~~ +- ~Merged DR v0.5.1.2 - Blind Prison shuffled outside TT/Keylogic Improvements~ ### 0.1.9.1 - Fixed logic issue with leaving IP entrance not requiring flippers -- ~~Merged DR v0.5.1.1 - Map Indicator Fix/Boss Shuffle Bias/Shop Hints~~ +- ~Merged DR v0.5.1.1 - Map Indicator Fix/Boss Shuffle Bias/Shop Hints~ ### 0.1.9.0 - Expanded Crossed OW to four separate options, see Readme for details @@ -190,7 +197,7 @@ - Fixed issues with Link/Bunny state in Crossed OW - Fixed issue with Standard+Parallel not using vanilla connections for Escape - Fixed issue with Mystery for OW boolean options -- ~~Merged DR v0.5.1.0 - Major Keylogic Update~~ +- ~Merged DR v0.5.1.0 - Major Keylogic Update~ ### 0.1.8.1 - Fixed issue with activating flute in DW (OW Mixed) @@ -205,12 +212,12 @@ - Added OW Shuffle support for Plando module (needs user testing) - Fixed issue with Sanc start at TR as bunny when it is LW - Fixed issue with Pyramid Hole not getting shuffled -- ~~Merged DR v0.5.0.3 - Minor DR fixes~~ +- ~Merged DR v0.5.0.3 - Minor DR fixes~ ### 0.1.7.4 - Fixed issue with Mixed OW failing to generate when HC/Pyramid is swapped - Various fixes to improve generation rates for Mixed OW Shuffle -- ~~Merged DR v0.5.0.2 - Shuffle SFX~~ +- ~Merged DR v0.5.0.2 - Shuffle SFX~ ### 0.1.7.3 - Fixed minor issue with ambient SFX stopping and starting on OW screen load @@ -230,10 +237,10 @@ ### 0.1.7.0 - Expanded new DR bomb logic to all modes (bomb usage in logic only if there is an unlimited supply of bombs available) -- ~~Merged DR v0.5.0.1 - Bombbag mode / Enemizer fixes~~ +- ~Merged DR v0.5.0.1 - Bombbag mode / Enemizer fixes~ ### 0.1.6.9 -- ~~Merged DR v0.4.0.12 - Secure random update / Credits fix~~ +- ~Merged DR v0.4.0.12 - Secure random update / Credits fix~ ### 0.1.6.8 - Implemented a smarter Balanced Flute Shuffle algorithm @@ -247,14 +254,14 @@ - Fixed Boss Music when boss room is entered thru straight stairs - Suppressed in-dungeon music changes when DR is enabled - Fixed issue with Pyramid Exit exiting to wrong location in ER -- ~~Merged DR v0.4.0.11 - Various DR changes~~ +- ~Merged DR v0.4.0.11 - Various DR changes~ ### 0.1.6.6 -- ~~Merged DR v0.4.0.9 - P/C Indicator / Credits fix / CLI Hints Fix~~ +- ~Merged DR v0.4.0.9 - P/C Indicator / Credits fix / CLI Hints Fix~ ### 0.1.6.5 - Reduced chance of diagonal flute spot in Balanced -- ~~Merged DR v0.4.0.8 - Boss Indicator / Psuedo Boots / Quickswap Update / Credits Updates~~ +- ~Merged DR v0.4.0.8 - Boss Indicator / Psuedo Boots / Quickswap Update / Credits Updates~ ### 0.1.6.4 - Fixed Frogsmith and Stumpy and restored progression in these locations @@ -293,15 +300,15 @@ ### 0.1.5.0 - Added OW Tile Swap setting - Fixed horizontal VRAM visual loading glitch on megatiles -- ~~Merged DR v0.4.0.7 - Fast Credits / Reduced Flashing / Sprite Author in Credits~~ +- ~~Merged DR v0.4.0.7 - Fast Credits / Reduced Flashing / Sprite Author in Credits~~ Didn't fully merge ### 0.1.4.3 -- Merged DR v0.4.0.6 - TT Maiden Attic Hint / DR Entrance Floor Mat Mods / Hard/Expert Item Pool Fix +- ~Merged DR v0.4.0.6 - TT Maiden Attic Hint / DR Entrance Floor Mat Mods / Hard/Expert Item Pool Fix~ ### 0.1.4.2 - Modified various OW map terrain specific to OW Shuffle - Changed World check to table-based vs OW ID-based (should have no effect with current modes) -- Merged DR v0.4.0.5 - Mystery Boss Shuffle Fix / Swordless+Hard Item Pool Fix / Insanity+Inverted ER Fixes +- ~Merged DR v0.4.0.5 - Mystery Boss Shuffle Fix / Swordless+Hard Item Pool Fix / Insanity+Inverted ER Fixes~ ### 0.1.4.1 - Moved Inverted Pyramid Entrance to top of HC Ledge @@ -315,11 +322,11 @@ - Various logic fixes and region prep for Inverted - Fixed muted MSU-1 music in door rando when descending GT Climb stairs - Fixed Standard + Vanilla (thanks compiling) -- Merged DR v0.4.0.4 - Shuffle Link's House / Experimental Bunny Start / 10 Bomb Fix +- ~Merged DR v0.4.0.4 - Shuffle Link's House / Experimental Bunny Start / 10 Bomb Fix~ ### 0.1.3.0 - Added OWG Logic for OW Shuffle -- Merged DR v0.4.0.2 - OWG Framework / YAML +- ~Merged DR v0.4.0.2 - OWG Framework / YAML~ ### 0.1.2.2 - Re-purposed OW Shuffle setting to Layout Shuffle @@ -328,7 +335,7 @@ ### 0.1.2.1 - Made possible fix for Standard -- Merged DR v0.3.1.10 - Fixed Standard generation +- ~Merged DR v0.3.1.10 - Fixed Standard generation~ ### 0.1.2.0 - Added 'Parallel Worlds' toggle option @@ -338,7 +345,7 @@ ### 0.1.1.2 - If Link's current position fits within the incoming gap, Link will not get re-centered to the incoming gap - Added Rule for Pearl required to drop down back of SW -- Merged DR v0.3.1.8 - Improved Shopsanity pricing - Fixed Retro generation +- ~Merged DR v0.3.1.8 - Improved Shopsanity pricing - Fixed Retro generation~ ### 0.1.1.1 - Fixed camera unlocking issue diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 347fc292..5a940200 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.7.3' +version_number = '0.2.8.0' version_branch = '-u' __version__ = '%s%s' % (version_number, version_branch) From 79f35582e8a0a6cf8466eebcdf830afa6a50f75e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 20 Jul 2022 21:57:06 -0500 Subject: [PATCH 208/293] Adding Zelda Delivered rule to Agahnim 1 rules if Standard --- BaseClasses.py | 11 +++++++---- Rules.py | 14 +++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5073d6cd..2a214716 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1110,7 +1110,7 @@ class CollectionState(object): for region in tree_pulls: if can_reach_non_bunny(region): return True - if not self.has('Beat Agahnim 1', player): + if not self.has_beaten_aga(player): for region in pre_aga_tree_pulls: if can_reach_non_bunny(region): return True @@ -1125,7 +1125,7 @@ class CollectionState(object): for region in bush_crabs: if can_reach_non_bunny(region): return True - if not self.has('Beat Agahnim 1', player): + if not self.has_beaten_aga(player): for region in pre_aga_bush_crabs: if can_reach_non_bunny(region): return True @@ -1202,7 +1202,7 @@ class CollectionState(object): for region in tree_pulls: if can_reach_non_bunny(region): return True - if not self.has('Beat Agahnim 1', player): + if not self.has_beaten_aga(player): for region in pre_aga_tree_pulls: if can_reach_non_bunny(region): return True @@ -1217,7 +1217,7 @@ class CollectionState(object): for region in bush_crabs: if can_reach_non_bunny(region): return True - if not self.has('Beat Agahnim 1', player): + if not self.has_beaten_aga(player): for region in pre_aga_bush_crabs: if can_reach_non_bunny(region): return True @@ -1343,6 +1343,9 @@ class CollectionState(object): self.is_not_bunny(cave, player) ) + def has_beaten_aga(self, player): + return self.has_beaten_aga(player) and (self.world.mode[player] != 'standard' or self.has('Zelda Delivered', player)) + def has_sword(self, player): return self.has('Fighter Sword', player) or self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player) diff --git a/Rules.py b/Rules.py index 35572ec5..933951f6 100644 --- a/Rules.py +++ b/Rules.py @@ -54,7 +54,7 @@ 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('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) + 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)) elif world.goal[player] == 'ganon': # require aga2 to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) @@ -807,7 +807,7 @@ def pot_rules(world, player): def default_rules(world, player): - set_rule(world.get_entrance('Other World S&Q', player), lambda state: state.has_Mirror(player) and state.has('Beat Agahnim 1', player)) + set_rule(world.get_entrance('Other World S&Q', player), lambda state: state.has_Mirror(player) and state.has_beaten_aga(player)) # Underworld Logic set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up @@ -950,7 +950,7 @@ def ow_rules(world, player): if world.is_atgt_swapped(player): set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) else: - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has_beaten_aga(player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('GT Entry Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'crossed', 'insanity')) @@ -1117,8 +1117,8 @@ def ow_rules(world, player): set_rule(world.get_entrance('HC East Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Courtyard Left Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Area South Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has('Beat Agahnim 1', player)) + set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has_beaten_aga(player)) + set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has_beaten_aga(player)) else: set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: world.is_pyramid_open(player) or state.has('Beat Agahnim 2', player)) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: False) @@ -1130,7 +1130,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Pyramid Uncle Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Pyramid From Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Pyramid Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Post Aga Inverted Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) + set_rule(world.get_entrance('Post Aga Inverted Teleporter', player), lambda state: state.has_beaten_aga(player)) if not world.is_tile_swapped(0x1d, player): set_rule(world.get_entrance('Wooden Bridge Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1620,7 +1620,7 @@ def swordless_rules(world, player): set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop if not world.is_atgt_swapped(player): - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has_beaten_aga(player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock Ledge', 'Region', player)) # sword not required to use medallion for opening in swordless (!) From fb02ae0159cf81f00206a4497ad62aa643bf2c31 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 21 Jul 2022 10:46:56 -0500 Subject: [PATCH 209/293] Adding Zelda Delivered rule to Agahnim 1 rules if Standard --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 2a214716..5bd55485 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1344,7 +1344,7 @@ class CollectionState(object): ) def has_beaten_aga(self, player): - return self.has_beaten_aga(player) and (self.world.mode[player] != 'standard' or self.has('Zelda Delivered', player)) + return self.has('Beat Agahnim 1', player) and (self.world.mode[player] != 'standard' or self.has('Zelda Delivered', player)) def has_sword(self, player): return self.has('Fighter Sword', player) or self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player) From ce9cb8de2647ab2a6de66b499f82d0edf9d552de Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 22 Jul 2022 10:55:35 -0500 Subject: [PATCH 210/293] 619 --- Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index 933951f6..c83f1733 100644 --- a/Rules.py +++ b/Rules.py @@ -824,7 +824,7 @@ def default_rules(world, player): set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) # Entrance Access - set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has_Boots(player) and state.has('Beat Agahnim 1', player)) + set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has_Boots(player) and state.has_beaten_aga(player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has_Boots(player)) From 360a61a65c3e59a4e28e278a10ec43f0a6c1524d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 22 Jul 2022 10:57:28 -0500 Subject: [PATCH 211/293] Fixed issue with pre-opened pyramid when it shouldn't be --- Rom.py | 2 +- Rules.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index add74a53..b614e730 100644 --- a/Rom.py +++ b/Rom.py @@ -1390,7 +1390,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - if world.open_pyramid[player]: + if world.is_pyramid_open(player): rom.initial_sram.pre_open_pyramid_hole() if world.crystals_needed_for_gt[player] == 0: rom.initial_sram.pre_open_ganons_tower() diff --git a/Rules.py b/Rules.py index c83f1733..45aa6ba6 100644 --- a/Rules.py +++ b/Rules.py @@ -1109,7 +1109,7 @@ def ow_rules(world, player): if not world.is_tile_swapped(0x1b, player): set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: False) set_rule(world.get_entrance('Inverted Pyramid Entrance', player), lambda state: False) - set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.open_pyramid[player] or state.has('Beat Agahnim 2', player)) + set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.is_pyramid_open(player) or state.has('Beat Agahnim 2', player)) set_rule(world.get_entrance('HC Area Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) From a5729b51a242d829512bb5f15e144adcb39ffa41 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 12:42:08 -0500 Subject: [PATCH 212/293] Adding new option for Bonk Drop Shuffle --- BaseClasses.py | 2 +- CLI.py | 3 ++- Main.py | 2 ++ Mystery.py | 1 + resources/app/cli/args.json | 4 ++++ resources/app/cli/lang/en.json | 3 +++ resources/app/gui/lang/en.json | 2 ++ resources/app/gui/randomize/overworld/widgets.json | 6 ++++++ source/classes/constants.py | 1 + source/gui/randomize/overworld.py | 8 ++++++-- 10 files changed, 28 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5bd55485..01825043 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -54,7 +54,7 @@ class World(object): self._entrance_cache = {} self._location_cache = {} self.required_locations = [] - self.shuffle_bonk_prizes = False + self.shuffle_bonk_drops = {} self.light_world_light_cone = False self.dark_world_light_cone = False self.clock_mode = 'none' diff --git a/CLI.py b/CLI.py index 8ba224bd..4e7ad021 100644 --- a/CLI.py +++ b/CLI.py @@ -109,7 +109,7 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots']: + 'msu_resume', 'collection_rate', 'colorizepots', 'bonk_drops']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -159,6 +159,7 @@ def parse_settings(): "ow_mixed": False, "ow_whirlpool": False, "ow_fluteshuffle": "vanilla", + "bonk_drops": False, "shuffle": "vanilla", "shufflelinks": False, "overworld_map": "default", diff --git a/Main.py b/Main.py index ac5843e5..2ac286bf 100644 --- a/Main.py +++ b/Main.py @@ -91,6 +91,7 @@ def main(args, seed=None, fish=None): world.owKeepSimilar = args.ow_keepsimilar.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy() + world.shuffle_bonk_drops = args.bonk_drops.copy() world.open_pyramid = args.openpyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_shuffle = args.shuffleenemies.copy() @@ -438,6 +439,7 @@ def copy_world(world, partial_copy=False): ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() ret.boss_shuffle = world.boss_shuffle.copy() ret.enemy_shuffle = world.enemy_shuffle.copy() diff --git a/Mystery.py b/Mystery.py index 07955bff..60a3f064 100644 --- a/Mystery.py +++ b/Mystery.py @@ -174,6 +174,7 @@ def roll_settings(weights): ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on' overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' + ret.shuffle_bonk_drops = get_choice('bonk_drops') == 'on' entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' overworld_map = get_choice('overworld_map') diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 2c169dcf..ebeedf53 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -167,6 +167,10 @@ "action": "store_true", "type": "bool" }, + "bonk_drops": { + "action": "store_true", + "type": "bool" + }, "ow_fluteshuffle": { "choices": [ "vanilla", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 802acf1e..5d753a66 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -234,6 +234,9 @@ "ow_whirlpool": [ "Whirlpools will be shuffled and paired together." ], + "bonk_drops": [ + "Bonk drops from trees, rocks, and statues are shuffled with the item pool." + ], "ow_fluteshuffle": [ "This randomizes the flute spot destinations.", "Vanilla: All flute spots remain unchanged.", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index b325429c..b25f4233 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -145,6 +145,8 @@ "randomizer.overworld.whirlpool": "Whirlpool Shuffle", + "randomizer.overworld.bonk_drops": "Bonk Drops", + "randomizer.overworld.overworldflute": "Flute Shuffle", "randomizer.overworld.overworldflute.vanilla": "Vanilla", "randomizer.overworld.overworldflute.balanced": "Balanced", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index 43dc9394..9595ea8e 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -1,4 +1,10 @@ { + "topOverworldFrame": { + "bonk_drops": { + "type": "checkbox", + "default": false + } + }, "leftOverworldFrame": { "overworldshuffle": { "type": "selectbox", diff --git a/source/classes/constants.py b/source/classes/constants.py index c120734b..1b6a6aa5 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -81,6 +81,7 @@ SETTINGSTOPROCESS = { "keepsimilar": "ow_keepsimilar", "mixed": "ow_mixed", "whirlpool": "ow_whirlpool", + "bonk_drops": "bonk_drops", "overworldflute": "ow_fluteshuffle" }, "entrance": { diff --git a/source/gui/randomize/overworld.py b/source/gui/randomize/overworld.py index 3cc3eb36..d3796af6 100644 --- a/source/gui/randomize/overworld.py +++ b/source/gui/randomize/overworld.py @@ -15,12 +15,16 @@ def overworld_page(parent): # Load Overworld Shuffle option widgets as defined by JSON file # Defns include frame name, widget type, widget options, widget placement attributes - # These get split left & right + self.frames["topOverworldFrame"] = Frame(self) self.frames["leftOverworldFrame"] = Frame(self) self.frames["rightOverworldFrame"] = Frame(self) + self.frames["topOverworldFrame"].pack(side=TOP, anchor=NW) self.frames["leftOverworldFrame"].pack(side=LEFT, anchor=NW, fill=Y) self.frames["rightOverworldFrame"].pack(anchor=NW, fill=Y) + + shuffleLabel = Label(self.frames["topOverworldFrame"], text="Shuffle: ") + shuffleLabel.pack(side=LEFT) with open(os.path.join("resources","app","gui","randomize","overworld","widgets.json")) as overworldWidgets: myDict = json.load(overworldWidgets) @@ -33,7 +37,7 @@ def overworld_page(parent): packAttrs = {"side":LEFT, "pady":(18,0)} elif key == "overworldflute": packAttrs["pady"] = (20,0) - elif key in ["whirlpool", "mixed"]: + elif key in ["mixed", "whirlpool"]: packAttrs = {"anchor":W, "padx":(79,0)} self.widgets[key].pack(packAttrs) From 97455dc1406727c825c6454282a8b6ef2d5ec7de Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 12:50:37 -0500 Subject: [PATCH 213/293] Implemented Bonk Drop Shuffle --- BaseClasses.py | 8 +- Fill.py | 29 +++--- ItemList.py | 33 ++++++- Items.py | 8 +- Regions.py | 49 +++++++++- Rom.py | 65 +++++++++---- Rules.py | 14 +++ Tables.py | 32 ++++--- asm/owrando.asm | 199 +++++++++++++++++++++++++++++++++++++++- data/base2current.bps | Bin 99252 -> 103920 bytes mystery_example.yml | 3 + source/item/FillUtil.py | 4 +- 12 files changed, 389 insertions(+), 55 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 01825043..4a484834 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1080,6 +1080,9 @@ class CollectionState(object): return True return False + def can_collect_bonkdrops(self, player): + return self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player)) + def can_farm_rupees(self, player): tree_pulls = ['Lost Woods East Area', 'Snitch Lady (East)', @@ -1192,7 +1195,7 @@ class CollectionState(object): if can_reach_non_bunny(region): return True - if self.has_Boots(player): + if not self.world.shuffle_bonk_drops[player] and self.can_collect_bonkdrops(player): for region in bonk_bombs: if can_reach_non_bunny(region): return True @@ -2696,6 +2699,7 @@ class LocationType(FastEnum): Shop = 3 Pot = 4 Drop = 5 + Bonk = 6 class Item(object): @@ -2911,6 +2915,7 @@ class Spoiler(object): 'ow_mixed': self.world.owMixed, 'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle, + 'bonk_drops': self.world.shuffle_bonk_drops, 'shuffle': self.world.shuffle, 'shuffleganon': self.world.shuffle_ganon, 'shufflelinks': self.world.shufflelinks, @@ -3123,6 +3128,7 @@ class Spoiler(object): outfile.write('Swapped OW (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('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) 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])) diff --git a/Fill.py b/Fill.py index 9bf7f98f..fe9cbcf9 100644 --- a/Fill.py +++ b/Fill.py @@ -355,18 +355,23 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # handle pot shuffle pots_used = False pot_item_pool = collections.defaultdict(list) - for item in world.itempool: - if item.name in ['Chicken', 'Big Magic']: # can only fill these in that players world - pot_item_pool[item.player].append(item) - for player, pot_pool in pot_item_pool.items(): - if pot_pool: - for pot_item in pot_pool: - world.itempool.remove(pot_item) - pot_locations = [location for location in fill_locations - if location.type == LocationType.Pot and location.player == player] - pot_locations = filter_pot_locations(pot_locations, world) - fast_fill_helper(world, pot_pool, pot_locations) - pots_used = True + + # guarantee one big magic in a bonk location + for player in range(1, world.players + 1): + if world.shuffle_bonk_drops[player]: + for item in world.itempool: + if item.name in ['Big Magic']: + pot_item_pool[player].append(item) + break + + for player, magic_pool in pot_item_pool.items(): + world.itempool.remove(magic_pool[0]) + pot_locations = [location for location in fill_locations + if location.type == LocationType.Bonk and location.player == player] + pot_locations = filter_pot_locations(pot_locations, world) + fast_fill_helper(world, magic_pool, pot_locations) + pots_used = True + if pots_used: fill_locations = world.get_unfilled_locations() random.shuffle(fill_locations) diff --git a/ItemList.py b/ItemList.py index 18472fb0..7350ff9a 100644 --- a/ItemList.py +++ b/ItemList.py @@ -3,11 +3,12 @@ import logging import math import RaceRandom as random -from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem +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 Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool from PotShuffle import vanilla_pots +from Tables import bonk_prize_lookup from Items import ItemFactory from source.item.FillUtil import trash_items, pot_items @@ -411,6 +412,10 @@ def generate_itempool(world, player): if world.pottery[player] not in ['none', 'keys']: add_pot_contents(world, player) + if world.shuffle_bonk_drops[player]: + create_dynamic_bonkdrop_locations(world, player) + add_bonkdrop_contents(world, player) + take_any_locations = [ 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut', @@ -500,6 +505,21 @@ def create_dynamic_shop_locations(world, player): loc.locked = True +def create_dynamic_bonkdrop_locations(world, player): + from Regions import bonk_prize_table + for bonk_location, (_, _, _, region_name, hint_text) in bonk_prize_table.items(): + region = world.get_region(region_name, player) + loc = Location(player, bonk_location, 0, region, hint_text) + loc.type = LocationType.Bonk + loc.parent_region = region + loc.address = 0x2abb00 + (bonk_prize_table[loc.name][0] * 6) + 3 + + region.locations.append(loc) + world.dynamic_locations.append(loc) + + world.clear_location_cache() + + def fill_prizes(world, attempts=15): all_state = world.get_all_state(keys=True) for player in range(1, world.players + 1): @@ -779,6 +799,17 @@ def add_pot_contents(world, player): world.itempool.append(ItemFactory(item, player)) +def add_bonkdrop_contents(world, player): + from Items import item_table + for item_name, (_, count, alt_item) in bonk_prize_lookup.items(): + if item_name not in item_table: + item_name = alt_item + while (count > 0): + item = ItemFactory(item_name, player) + world.itempool.append(item) + count -= 1 + + def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, flute_activated): pool = [] placed_items = {} diff --git a/Items.py b/Items.py index ea81c136..10d93159 100644 --- a/Items.py +++ b/Items.py @@ -81,10 +81,10 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche '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'), 'Arrow Upgrade (+5)': (False, False, None, 0x53, 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'), 'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), - 'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), + 'Arrows (5)': (False, False, None, 0xB5, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), 'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), - 'Big Magic': (False, False, None, 0x5A, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), - 'Chicken': (False, False, None, 0x5A, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), + 'Big Magic': (False, False, None, 0xB4, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), + 'Chicken': (False, False, None, 0xB3, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), 'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), 'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), @@ -177,6 +177,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Blue Potion': (False, False, None, 0x30, 160, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'), 'Bee': (False, False, None, 0x0E, 10, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'), '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, 0xB1, 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, 0xB2, 50, 'Just a pixie!', 'and the pixie', 'the pixie kid', 'pixie for sale', 'pixie fungus', 'bottle boy has pixie again', 'a pixie'), '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), diff --git a/Regions.py b/Regions.py index 18c14c9a..af3c13ef 100644 --- a/Regions.py +++ b/Regions.py @@ -1266,7 +1266,54 @@ def pot_address(pot_index, super_tile): return 0x7f6018 + super_tile * 2 + (pot_index << 24) -# (type, room_id, shopkeeper, custom, locked, [items]) +# bonk location: record id, aga required, default item, region, hint text +bonk_prize_table = { + 'Lost Woods Hideout Tree': (0x00, False, '', 'Lost Woods East Area', 'in a tree'), + 'Death Mountain Bonk Rocks': (0x01, False, '', 'East Death Mountain (Top East)', 'encased in stone'), + 'Mountain Entry Pull Tree': (0x02, False, '', 'Mountain Entry Area', 'in a tree'), + 'Mountain Entry Southeast Tree': (0x03, False, '', 'Mountain Entry Area', 'in a tree'), + 'Lost Woods Pass West Tree': (0x04, False, '', 'Lost Woods Pass West Area', 'in a tree'), + 'Kakariko Portal Tree': (0x05, False, '', 'Lost Woods Pass East Top Area', 'in a tree'), + 'Fortune Bonk Rocks': (0x06, False, '', 'Kakariko Fortune Area', 'in a tree'), + 'Kakariko Pond Tree': (0x07, True, '', 'Kakariko Pond Area', 'in a tree'), + 'Bonk Rocks Tree': (0x08, True, '', 'Bonk Rock Ledge', 'in a tree'), + 'Sanctuary Tree': (0x09, False, '', 'Sanctuary Area', 'in a tree'), + 'River Bend West Tree': (0x0a, True, '', 'River Bend Area', 'in a tree'), + 'River Bend East Tree': (0x0b, False, '', 'River Bend East Bank', 'in a tree'), + 'Blinds Hideout Tree': (0x0c, False, '', 'Kakariko Area', 'in a tree'), + 'Kakariko Welcome Tree': (0x0d, False, '', 'Kakariko Area', 'in a tree'), + 'Forgotten Forest Southwest Tree': (0x0e, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Forgotten Forest Central Tree': (0x0f, False, '', 'Forgotten Forest Area', 'in a tree'), + #'Forgotten Forest Southeast Tree': (0x??, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Hyrule Castle Tree': (0x10, False, '', 'Hyrule Castle Courtyard', 'in a tree'), + 'Wooden Bridge Tree': (0x11, False, '', 'Wooden Bridge Area', 'in a tree'), + 'Eastern Palace Tree': (0x12, True, '', 'Eastern Palace Area', 'in a tree'), + 'Flute Boy South Tree': (0x13, True, '', 'Flute Boy Area', 'in a tree'), + 'Flute Boy East Tree': (0x14, True, '', 'Flute Boy Area', 'in a tree'), + 'Central Bonk Rocks Tree': (0x15, False, '', 'Central Bonk Rocks Area', 'in a tree'), + 'Tree Line Tree 2': (0x16, True, '', 'Tree Line Area', 'in a tree'), + 'Tree Line Tree 4': (0x17, True, '', 'Tree Line Area', 'in a tree'), + 'Flute Boy Approach South Tree': (0x18, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Flute Boy Approach North Tree': (0x19, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Dark Lumberjack Tree': (0x1a, False, '', 'Dark Lumberjack Area', 'in a tree'), + 'Dark Fortune Bonk Rocks (Drop 1)': (0x1b, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Fortune Bonk Rocks (Drop 2)': (0x1c, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Graveyard West Bonk Rocks': (0x1d, False, '', 'Dark Graveyard Area', 'encased in stone'), + 'Dark Graveyard North Bonk Rocks': (0x1e, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Dark Graveyard Tomb Bonk Rocks': (0x1f, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Qirn Jump West Tree': (0x20, False, '', 'Qirn Jump Area', 'in a tree'), + 'Qirn Jump East Tree': (0x21, False, '', 'Qirn Jump East Bank', 'in a tree'), + 'Dark Witch Tree': (0x22, False, '', 'Dark Witch Area', 'in a tree'), + 'Pyramid Tree': (0x23, False, '', 'Pyramid Area', 'in a tree'), + 'Palace of Darkness Tree': (0x24, False, '', 'Palace of Darkness Area', 'in a tree'), + 'Dark Tree Line Tree 2': (0x25, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 3': (0x26, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 4': (0x27, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Hype Cave Statue': (0x28, False, '', 'Hype Cave Area', 'encased in stone') +} + + +# (room_id, type, shopkeeper, custom, locked, [items]) # item = (item, price, max=0, replacement=None, replacement_price=0) _basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] _dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] diff --git a/Rom.py b/Rom.py index b614e730..7c43ea9f 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '210e4631353e3d094f01bf91562844a5' +RANDOMIZERBASEHASH = 'f76555dcc8cbd0f185fb37eafa3779c3' class JsonRom(object): @@ -630,6 +630,14 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(sprite_pointer+1, 0) rom.write_byte(sprite_pointer+2, code) continue + elif location.type == LocationType.Bonk: + address = snes_to_pc(location.address) + rom.write_byte(address, handle_native_dungeon(location, itemid)) + if location.item.player != player: + rom.write_byte(address+1, location.item.player) + else: + rom.write_byte(address+1, 0) + continue if location.address is None or (type(location.address) is int and location.address >= 0x400000): continue @@ -771,18 +779,42 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set world flag world_flag = 0x00 if b >= 0x40 and b < 0x80 else 0x40 - rom.write_byte(0x153A00 + b, world_flag) + rom.write_byte(0x1539B0 + b, world_flag) if b & 0xBF in megatiles: - rom.write_byte(0x153A00 + b + 1, world_flag) - rom.write_byte(0x153A00 + b + 8, world_flag) - rom.write_byte(0x153A00 + b + 9, world_flag) + rom.write_byte(0x1539B0 + b + 1, world_flag) + rom.write_byte(0x1539B0 + b + 8, world_flag) + rom.write_byte(0x1539B0 + b + 9, world_flag) for edge in world.owedges: if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) if not edge.specialExit: - rom.write_byte(0x1539e0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) + rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) + # patch bonk prizes + if world.shuffle_bonk_drops: + bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, + 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] + bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, + 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51, + 0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7, + 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] + + # # legacy bonk prize shuffle, shuffles bonk prizes amongst themselves + # random.shuffle(bonk_prizes) + # for prize, address in zip(bonk_prizes, bonk_addresses): + # rom.write_byte(address, prize) + + owFlags |= 0x200 + + # setting spriteID to D8, a placeholder sprite we use to inform ROM to spawn a dynamic item + #for address in bonk_addresses: + for address in [b for b in bonk_addresses if b != 0x4D0AE]: # temp fix for screen 1A murahdahla sprite replacement + rom.write_byte(address, 0xD8) + # temporary fix for screen 1A + rom.write_byte(snes_to_pc(0x09AE32), 0xD8) + rom.write_byte(snes_to_pc(0x09AE35), 0xD8) + write_int16(rom, 0x150002, owMode) write_int16(rom, 0x150004, owFlags) @@ -875,6 +907,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item) 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]))] valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) @@ -1251,18 +1284,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # fill enemy prize packs rom.write_bytes(0x37A78, pack_prizes) - # set bonk prizes - bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, - 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] - bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, - 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51, - 0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7, - 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] - if world.shuffle_bonk_prizes: - random.shuffle(bonk_prizes) - for prize, address in zip(bonk_prizes, bonk_addresses): - rom.write_byte(address, prize) - # Fill in item substitutions table rom.write_bytes(0x184000, [ # original_item, limit, replacement_item, filler @@ -1644,6 +1665,12 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # temporarally we are just nopping out this check we will conver this to a rom fix soon. rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) + # sprite patches + rom.write_byte(snes_to_pc(0x0db7d1), 0x03) # patch apple sprites to not permadeatch like enemies + if world.shuffle_bonk_drops[player]: + # warning, this temporary patch might cause fairies to respawn differently?, limiting this to bonk drop mode only + rom.write_byte(snes_to_pc(0x0db808), 0x03) # patch fairies sprites to not permadeatch like enemies + # allow smith into multi-entrance caves in appropriate shuffles if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) @@ -2754,7 +2781,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): # apply inverted map changes for b in range(0x00, len(inverted_buffer)): - rom.write_byte(0x153B00 + b, inverted_buffer[b]) + rom.write_byte(0x153A70 + b, inverted_buffer[b]) def patch_shuffled_dark_sanc(world, rom, player): dark_sanc = world.get_region('Dark Sanctuary Hint', player) diff --git a/Rules.py b/Rules.py index 45aa6ba6..9619c897 100644 --- a/Rules.py +++ b/Rules.py @@ -823,6 +823,15 @@ def default_rules(world, player): set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) + # Bonk Item Access + if world.shuffle_bonk_drops[player]: + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + from Regions import bonk_prize_table + for location_name, (_, aga_required, _, _, _) in bonk_prize_table.items(): + loc = world.get_location(location_name, player) + set_rule(loc, lambda state: (state.can_collect_bonkdrops(player)) and (not aga_required or state.has_beaten_aga(player))) + add_bunny_rule(loc, player) + # Entrance Access set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has_Boots(player) and state.has_beaten_aga(player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has_Boots(player)) @@ -1731,6 +1740,11 @@ def standard_rules(world, player): add_rule(world.get_entrance('Hyrule Castle Ledge Drop', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Zelda Delivered', player)) + if world.shuffle_bonk_drops[player]: + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + add_rule(world.get_location('Hyrule Castle Tree', player), lambda state: state.has('Zelda Delivered', player)) + add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) + # don't allow bombs to get past here before zelda is rescued set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) diff --git a/Tables.py b/Tables.py index 64464b9f..a30f24f0 100644 --- a/Tables.py +++ b/Tables.py @@ -125,17 +125,23 @@ divisor_lookup = { # 0xf8: 0xbac, 0xf9: 0xbba, 0xfa: 0xbc1, 0xfb: 0xbcc, 0xfc: 0xbd7, 0xfd: 0xbd7, 0xfe: 0xbba, 0xff: 0xbe3 # } -prize_lookup = { - 0xd8: 'Small Magic Refill', - 0xd9: 'Rupee (1)', - 0xda: 'Rupees (5)', - 0xdb: 'Rupees (20)', - 0xdc: 'Bomb (1)', - 0xdd: 'Bombs (4)', - 0xde: 'Bombs (8)', - 0xdf: 'Heart', - 0xe0: 'Fairy', - 0xe1: 'Arrows (5)', - 0xe2: 'Arrows (10)', - 0xe3: 'Full Magic Refill' +# item name: (spriteID, pool count, replacement item) +bonk_prize_lookup = { + 'Chicken': (0x0b, 0, None), + 'Bee Trap': (0x79, 6, None), + 'Apples': (0xac, 8, None), + 'Small Heart': (0xd8, 2, None), + 'Rupee (1)': (0xd9, 0, None), + 'Rupees (5)': (0xda, 3, None), # TODO: add in murahdahla tree rupee + 'Rupees (20)': (0xdb, 3, None), + 'Single Bomb': (0xdc, 2, None), + 'Bombs (3)': (None, 0, 'Bombs (4)'), + 'Bombs (4)': (0xdd, 0, 'Bombs (3)'), + 'Bombs (8)': (0xde, 1, 'Bombs (10)'), + 'Bombs (10)': (None, 0, 'Bombs (8)'), + 'Small Magic': (0xdf, 0, None), + 'Big Magic': (0xe0, 1, None), + 'Arrows (5)': (0xe1, 0, None), + 'Arrows (10)': (0xe2, 0, None), + 'Fairy': (0xe3, 15, None) } diff --git a/asm/owrando.asm b/asm/owrando.asm index 2d0c1ebd..7793ce2d 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -143,6 +143,9 @@ jsl.l OWWorldCheck16 : nop org $02b16e ; AND #$3F : ORA 7EF3CA and #$7f : eor #$40 : nop #2 +org $06AD4C +jsl.l OWBonkDrops : nop #4 + ;Code org $aa8800 OWTransitionDirection: @@ -368,6 +371,136 @@ LoadMapDarkOrMixed: dw $0400+$0210 ; bottom right } +; Y = sprite slot index of bonk sprite +OWBonkDrops: +{ + CMP.b #$D8 : BEQ + + RTL + + LDA.l OWFlags+1 : AND.b #$02 : BNE + + JSL.l Sprite_TransmuteToBomb : RTL + + + + ; loop thru rando bonk table to find match + PHB : PHK : PLB + LDA.b $8A + LDX.b #(40*6) ; 40 bonk items, 6 bytes each + - CMP.w OWBonkPrizeData,X : BNE + + INX + LDA.w $0D10,Y : LSR A : LSR A : LSR A : LSR A + EOR.w $0D00,Y : CMP.w OWBonkPrizeData,X : BNE ++ ; X = row + 1 + BRA .found_match + ++ DEX : LDA.b $8A + + CPX.b #$00 : BNE + + PLB : RTL + + DEX : DEX : DEX : DEX : DEX : DEX : BRA - + + .found_match + INX : LDA.w OWBonkPrizeData,X : PHX : PHA ; S = FlagBitmask, X (row + 2) + LDX.b $8A : LDA.l OverworldEventDataWRAM,X : AND 1,S : PHA : BNE + ; S = Collected, FlagBitmask, X (row + 2) + LDA.b #$1B : STA $12F ; JSL Sound_SetSfx3PanLong ; seems that when you bonk, there is a pending bonk sfx, so we clear that out and replace with reveal secret sfx + + + LDA 3,S : TAX : INX : LDA.w OWBonkPrizeData,X + PHA : INX : LDA.w OWBonkPrizeData,X : BEQ + + ; multiworld item + DEX : PLA ; X = row + 3 + JMP .spawn_item + + DEX : PLA ; X = row + 3 + + .determine_type ; A = item id ; S = Collected, FlagBitmask, X (row + 2) + CMP.b #$B0 : BNE + + LDA.b #$79 : JMP .sprite_transform ; transform to bees + + CMP.b #$42 : BNE + + JSL.l Sprite_TransmuteToBomb ; transform a heart to bomb, vanilla behavior + JMP .mark_collected + + CMP.b #$34 : BNE + + LDA.b #$D9 : CLC : JMP .sprite_transform ; transform to single rupee + + CMP.b #$35 : BNE + + LDA.b #$DA : CLC : BRA .sprite_transform ; transform to blue rupee + + CMP.b #$36 : BNE + + LDA.b #$DB : CLC : BRA .sprite_transform ; transform to red rupee + + CMP.b #$27 : BNE + + LDA.b #$DC : CLC : BRA .sprite_transform ; transform to 1 bomb + + CMP.b #$28 : BNE + + LDA.b #$DD : CLC : BRA .sprite_transform ; transform to 4 bombs + + CMP.b #$31 : BNE + + LDA.b #$DE : CLC : BRA .sprite_transform ; transform to 8 bombs + + CMP.b #$45 : BNE + + LDA.b #$DF : CLC : BRA .sprite_transform ; transform to small magic + + CMP.b #$B4 : BNE + + LDA.b #$E0 : CLC : BRA .sprite_transform ; transform to big magic + + CMP.b #$B5 : BNE + + LDA.b #$E1 : CLC : BRA .sprite_transform ; transform to 5 arrows + + CMP.b #$44 : BNE + + LDA.b #$E2 : CLC : BRA .sprite_transform ; transform to 10 arrows + + CMP.b #$B1 : BNE + + LDA.b #$AC : BRA .sprite_transform ; transform to apples + + CMP.b #$B2 : BNE + + LDA.b #$E3 : BRA .sprite_transform ; transform to fairy + + CMP.b #$B3 : BNE .spawn_item + INX : INX : LDA.w OWBonkPrizeData,X ; X = row + 5 + CLC : ADC.b #$08 : PHA + LDA.w $0D00,Y : SEC : SBC.b 1,S : STA.w $0D00,Y + LDA.w $0D20,Y : SBC.b #$00 : STA.w $0D20,Y : PLX + LDA.b #$0B : SEC ; BRA .sprite_transform ; transform to chicken + + .sprite_transform + STA.w $0E20,Y + TYX : JSL.l Sprite_LoadProperties + BEQ + + ; these are sprite properties that make it fall out of the tree to the east + LDA #$30 : STA $0F80,Y ; amount of force (related to speed) + LDA #$10 : STA $0D50,Y ; eastward rate of speed + LDA #$FF : STA $0B58,Y ; expiration timer + + + + .mark_collected ; S = Collected, FlagBitmask, X (row + 2) + PLA : BNE + ; S = FlagBitmask, X (row + 2) + LDX.b $8A : LDA.l OverworldEventDataWRAM,X : ORA 1,S : STA.l OverworldEventDataWRAM,X + + REP #$20 + LDA.l TotalItemCounter : INC : STA.l TotalItemCounter + SEP #$20 + + JMP .return + + ; spawn itemget item + .spawn_item ; A = item id ; Y = tree sprite slot ; S = Collected, FlagBitmask, X (row + 2) + PLX : BEQ + : LDA.b #$00 : STA.w $0DD0,Y : JMP .return ; S = FlagBitmask, X (row + 2) + + LDA 2,S : TAX : INX : INX + LDA.w OWBonkPrizeData,X : STA.l !MULTIWORLD_SPRITEITEM_PLAYER_ID + DEX + + LDA.b #$01 : STA !REDRAW + + LDA.b #$EB + STA.l $7FFE00 + JSL Sprite_SpawnDynamically+15 ; +15 to skip finding a new slot, use existing sprite + + ; affects the rate the item moves in the Y/X direction + LDA.b #$00 : STA.w $0D40,Y + LDA.b #$0A : STA.w $0D50,Y + + LDA.b #$20 : STA.w $0F80,Y ; amount of force (gives height to the arch) + LDA.b #$FF : STA.w $0B58,Y ; stun timer + LDA.b #$30 : STA.w $0F10,Y ; aux delay timer 4 ?? dunno what that means + + LDA.b #$00 : STA.w $0F20,Y ; layer the sprite is on + + ; sets OW event bitmask flag, uses free RAM + PLA : STA.w $0ED0,Y ; S = X (row + 2) + + ; determines the initial spawn point of item + PLX : INX : INX : INX + LDA.w $0D00,Y : SEC : SBC.w OWBonkPrizeData,X : STA.w $0D00,Y + LDA.w $0D20,Y : SBC #$00 : STA.w $0D20,Y + + LDA.b #$01 : STA !REDRAW : STA !FORCE_HEART_SPAWN + + PLB : RTL + + .return + PLA : PLA : PLB : RTL +} + org $aa9000 OWDetectEdgeTransition: { @@ -1130,11 +1263,11 @@ dw $0f20, $0f40, $0020, $0f30, $757e, $0000, $0000, $0049 dw $0f70, $0fb8, $0048, $0f94, $757e, $0000, $0000, $004a dw $0058, $00c0, $0068, $008c, $8080, $0000, $0000, $0017 ;Hobo (unused) -org $aab9e0 ;PC 1539e0 +org $aab9a0 ;PC 1539a0 OWSpecialDestIndex: dw $0080, $0081, $0082 -org $aaba00 ;PC 153a00 +org $aab9b0 ;PC 1539b0 OWTileWorldAssoc: db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0, 0, 0, 0, 0, 0, 0 @@ -1154,7 +1287,7 @@ db $40, $40, $40, $40, $40, $40, $40, $40 db $40, $40, $40, $40, $40, $40, $40, $40 db $00, $00 -org $aabb00 ;PC 153b00 +org $aaba70 ;PC 153a70 OWTileMapAlt: db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0, 0, 0, 0, 0, 0, 0 @@ -1175,3 +1308,63 @@ db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0 + +org $aabb00 ;PC 153b00 +OWBonkPrizeData: +; OWID YX Flag Item MW Offset +db $00, $59, $10, $b0, $00, $20 +db $05, $04, $10, $b2, $00, $00 +db $0a, $4e, $10, $b0, $00, $20 +db $0a, $a9, $08, $b1, $00, $20 +db $10, $c7, $10, $b1, $00, $20 +db $10, $f7, $08, $b4, $00, $20 +db $11, $08, $10, $27, $00, $00 +db $12, $a4, $10, $b2, $00, $20 +db $13, $c7, $10, $31, $00, $20 +db $13, $98, $08, $b1, $00, $20 +db $15, $a4, $10, $b1, $00, $20 +db $15, $fb, $08, $b2, $00, $20 +db $18, $a8, $10, $b2, $00, $20 +db $18, $36, $08, $35, $00, $20 +db $1a, $8a, $10, $42, $00, $20 +db $1a, $1d, $08, $b2, $00, $20 +;db $1a, $77, $04, $35, $00, $20 ; pre aga ONLY ; hijacked murahdahla bonk tree +db $1b, $46, $10, $b1, $00, $10 +db $1d, $6b, $10, $b1, $00, $20 +db $1e, $72, $10, $b2, $00, $20 +db $2a, $8f, $10, $36, $00, $20 +db $2a, $45, $08, $36, $00, $20 +db $2b, $d6, $10, $b2, $00, $20 +db $2e, $9c, $10, $b2, $00, $20 +db $2e, $b4, $08, $b0, $00, $20 +db $32, $29, $10, $42, $00, $20 +db $32, $9a, $08, $b2, $00, $20 +db $42, $66, $10, $b2, $00, $20 +db $51, $08, $10, $b2, $00, $00 +db $51, $09, $08, $b2, $00, $00 +db $54, $b5, $10, $27, $00, $00 +db $54, $ef, $08, $b2, $00, $08 +db $54, $b9, $04, $36, $00, $00 +db $55, $aa, $10, $b0, $00, $20 +db $55, $fb, $08, $35, $00, $20 +db $56, $e4, $10, $b0, $00, $20 +db $5b, $a7, $10, $b2, $00, $20 +db $5e, $00, $10, $b2, $00, $20 +db $6e, $8c, $10, $35, $00, $10 +db $6e, $90, $08, $b0, $00, $10 +db $6e, $a4, $04, $b1, $00, $10 +db $74, $4e, $10, $b1, $00, $1c + +; temporary fix - murahdahla replaces one of the bonk tree prizes +; so we copy the sprite table here and update the pointer +; longterm solution should be to spawn in murahdahla separately +org $09AE2A +Overworld_Sprites_Screen1A_2: +db $08, $0F, $41 ; yx:{ 0x080, 0x0F0 } +db $0E, $0C, $41 ; yx:{ 0x0E0, 0x0C0 } +db $11, $0D, $E3 ; yx:{ 0x110, 0x0D0 } +db $18, $0A, $D8 ; yx:{ 0x180, 0x0A0 } +db $18, $0F, $45 ; yx:{ 0x180, 0x0F0 } +db $FF ; END +org $09CA55 +dw Overworld_Sprites_Screen1A_2&$FFFF \ No newline at end of file diff --git a/data/base2current.bps b/data/base2current.bps index 52e4e07c704403b489d94ed7510bf4ac972dc0f3..9e8c66e6f28d20e47f2a3afbf45daccad2b38606 100644 GIT binary patch delta 21133 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 OYk%|+{O{Nri~k270uTrQ diff --git a/mystery_example.yml b/mystery_example.yml index 41a46ca6..ab7ffe63 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -22,6 +22,9 @@ vanilla: 0 balanced: 1 random: 1 + bonk_drops: + on: 1 + off: 1 door_shuffle: vanilla: 0 basic: 2 diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 9acdb45b..bf48c755 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -792,8 +792,8 @@ trash_items = { 'Bee Trap': 0, 'Rupee (1)': 1, 'Rupees (5)': 1, 'Small Heart': 1, 'Bee': 1, 'Arrows (5)': 1, 'Chicken': 1, 'Single Bomb': 1, 'Rupees (20)': 2, 'Small Magic': 2, - 'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3, - 'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4, + 'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3, 'Apples': 3, + 'Fairy': 4, 'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4, 'Rupees (300)': 5, 'Piece of Heart': 17 } From db15fdfbdc81dfcb887987bf5b4592f4f55af184 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 13:49:28 -0500 Subject: [PATCH 214/293] Shifted AddReceivedItemExpanded data table down, will affect people patching itemgfx --- Rom.py | 2 +- data/base2current.bps | Bin 103920 -> 104024 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 7c43ea9f..7be52c23 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'f76555dcc8cbd0f185fb37eafa3779c3' +RANDOMIZERBASEHASH = '3c8a6a640f2e0e004c8a9e0e72310622' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 9e8c66e6f28d20e47f2a3afbf45daccad2b38606..2ec7b58c91d9d6abce9da52887ff53ffa9c11fc6 100644 GIT binary patch delta 9522 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 delta 9459 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 Date: Sun, 24 Jul 2022 13:56:36 -0500 Subject: [PATCH 215/293] Fixed wrong loot ID for 5 arrows items --- Rom.py | 2 +- data/base2current.bps | Bin 104024 -> 104024 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 7be52c23..2a68ea42 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '3c8a6a640f2e0e004c8a9e0e72310622' +RANDOMIZERBASEHASH = '603d2b84ca1c96fd47f5bba83a0c688c' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 2ec7b58c91d9d6abce9da52887ff53ffa9c11fc6..cc8b89d84f68d6a875e5e04cc5e93c123dae31fc 100644 GIT binary patch delta 38 ucmcbyhV8~0whf7l%)y`5Y))a^>CCvbS0NMZ8tOnSu2C!iP16A^>vt$9eLjkjc9!0kvMFBEV2>jz2xgL7t20kATq5uE@ From 58afaa52e8a1cfc6773b41efcfd7abc35b5fdbe7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 14:56:25 -0500 Subject: [PATCH 216/293] Fixed issue with bonk prize on screen 2B --- Rom.py | 2 +- data/base2current.bps | Bin 104024 -> 104010 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 2a68ea42..0acf2ad8 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '603d2b84ca1c96fd47f5bba83a0c688c' +RANDOMIZERBASEHASH = '528840ac74c94330e8bb70ce6d96161e' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index cc8b89d84f68d6a875e5e04cc5e93c123dae31fc..795be7ea926e1e80dc6fbde6d2e386a51d5c9775 100644 GIT binary patch delta 8612 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 delta 8572 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 QXMp(WL0d*YKYh*r0Z82coB#j- From a97ee6c57a776a6fbc4b233ab8234f6d20d7dd4d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 16:19:26 -0500 Subject: [PATCH 217/293] Implemented Bonk Drop Shuffle --- Fill.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index fe9cbcf9..80da0109 100644 --- a/Fill.py +++ b/Fill.py @@ -471,7 +471,7 @@ def calc_trash_locations(world, player): total_count, gt_count = 0, 0 for loc in world.get_locations(): if (loc.player == player and loc.item is None - and (loc.type not in {LocationType.Pot, LocationType.Drop, LocationType.Normal} or not loc.forced_item) + and (loc.type not in {LocationType.Bonk, LocationType.Pot, LocationType.Drop, LocationType.Normal} or not loc.forced_item) and (loc.type != LocationType.Shop or world.shopsanity[player]) and loc.parent_region.dungeon): total_count += 1 @@ -482,18 +482,22 @@ def calc_trash_locations(world, player): def ensure_good_pots(world, write_skips=False): for loc in world.get_locations(): - # convert Arrows 5 and Nothing when necessary - if (loc.item.name in {'Arrows (5)', 'Nothing'} + # # convert Arrows 5 when necessary + # if (loc.item.name in {'Arrows (5)'} + # and loc.type not in [LocationType.Pot, LocationType.Bonk]): + # loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player) + # convert Nothing when necessary + if (loc.item.name in {'Nothing'} and (loc.type != LocationType.Pot or loc.item.player != loc.player)): loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player) - # can be placed here by multiworld balancing or shop balancing - # change it to something normal for the player it got swapped to - elif (loc.item.name in {'Chicken', 'Big Magic'} - and (loc.type != LocationType.Pot or loc.item.player != loc.player)): - if loc.type == LocationType.Pot: - loc.item.player = loc.player - else: - loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player) + # # can be placed here by multiworld balancing or shop balancing + # # change it to something normal for the player it got swapped to + # elif (loc.item.name in {'Chicken', 'Big Magic'} + # and (loc.type != LocationType.Pot or loc.item.player != loc.player)): + # if loc.type == LocationType.Pot: + # loc.item.player = loc.player + # else: + # loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player) # do the arrow retro check if world.retro[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}: loc.item = ItemFactory('Rupees (5)', loc.item.player) From 9ef8f54e5bed89713e625cd1de330d1457c0c1c3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 16:21:21 -0500 Subject: [PATCH 218/293] Added Multiworld Support for Bonk Drops --- ItemList.py | 2 +- MultiClient.py | 14 ++++++-- Regions.py | 91 ++++++++++++++++++++++++++------------------------ Rules.py | 2 +- 4 files changed, 61 insertions(+), 48 deletions(-) diff --git a/ItemList.py b/ItemList.py index 7350ff9a..fdcf5412 100644 --- a/ItemList.py +++ b/ItemList.py @@ -507,7 +507,7 @@ def create_dynamic_shop_locations(world, player): def create_dynamic_bonkdrop_locations(world, player): from Regions import bonk_prize_table - for bonk_location, (_, _, _, region_name, hint_text) in bonk_prize_table.items(): + for bonk_location, (_, _, _, _, region_name, hint_text) in bonk_prize_table.items(): region = world.get_region(region_name, player) loc = Location(player, bonk_location, 0, region, hint_text) loc.type = LocationType.Bonk diff --git a/MultiClient.py b/MultiClient.py index fbde673d..fd0e8b4f 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -929,14 +929,22 @@ async def track_locations(ctx : Context, roomid, roomdata): ow_unchecked = {} for location, screenid in location_table_ow.items(): if location not in ctx.locations_checked: - ow_unchecked[location] = screenid + ow_unchecked[location] = (screenid, 0x40) + ow_begin = min(ow_begin, screenid) + ow_end = max(ow_end, screenid + 1) + from Regions import bonk_prize_table + from OWEdges import OWTileRegions + for location, (_, flag, _, _, region_name, _) in bonk_prize_table.items(): + if location not in ctx.locations_checked: + screenid = OWTileRegions[region_name] + ow_unchecked[location] = (screenid, flag) ow_begin = min(ow_begin, screenid) ow_end = max(ow_end, screenid + 1) if ow_begin < ow_end: ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin) if ow_data is not None: - for location, screenid in ow_unchecked.items(): - if ow_data[screenid - ow_begin] & 0x40 != 0: + for location, (screenid, flag) in ow_unchecked.items(): + if ow_data[screenid - ow_begin] & flag != 0: new_check(location) if not all([location in ctx.locations_checked for location in location_table_npc.keys()]): diff --git a/Regions.py b/Regions.py index af3c13ef..9e6f18ac 100644 --- a/Regions.py +++ b/Regions.py @@ -1266,52 +1266,55 @@ def pot_address(pot_index, super_tile): return 0x7f6018 + super_tile * 2 + (pot_index << 24) -# bonk location: record id, aga required, default item, region, hint text +# bonk location: record id, OW flag bitmask, aga required, default item, region, hint text bonk_prize_table = { - 'Lost Woods Hideout Tree': (0x00, False, '', 'Lost Woods East Area', 'in a tree'), - 'Death Mountain Bonk Rocks': (0x01, False, '', 'East Death Mountain (Top East)', 'encased in stone'), - 'Mountain Entry Pull Tree': (0x02, False, '', 'Mountain Entry Area', 'in a tree'), - 'Mountain Entry Southeast Tree': (0x03, False, '', 'Mountain Entry Area', 'in a tree'), - 'Lost Woods Pass West Tree': (0x04, False, '', 'Lost Woods Pass West Area', 'in a tree'), - 'Kakariko Portal Tree': (0x05, False, '', 'Lost Woods Pass East Top Area', 'in a tree'), - 'Fortune Bonk Rocks': (0x06, False, '', 'Kakariko Fortune Area', 'in a tree'), - 'Kakariko Pond Tree': (0x07, True, '', 'Kakariko Pond Area', 'in a tree'), - 'Bonk Rocks Tree': (0x08, True, '', 'Bonk Rock Ledge', 'in a tree'), - 'Sanctuary Tree': (0x09, False, '', 'Sanctuary Area', 'in a tree'), - 'River Bend West Tree': (0x0a, True, '', 'River Bend Area', 'in a tree'), - 'River Bend East Tree': (0x0b, False, '', 'River Bend East Bank', 'in a tree'), - 'Blinds Hideout Tree': (0x0c, False, '', 'Kakariko Area', 'in a tree'), - 'Kakariko Welcome Tree': (0x0d, False, '', 'Kakariko Area', 'in a tree'), - 'Forgotten Forest Southwest Tree': (0x0e, False, '', 'Forgotten Forest Area', 'in a tree'), - 'Forgotten Forest Central Tree': (0x0f, False, '', 'Forgotten Forest Area', 'in a tree'), - #'Forgotten Forest Southeast Tree': (0x??, False, '', 'Forgotten Forest Area', 'in a tree'), - 'Hyrule Castle Tree': (0x10, False, '', 'Hyrule Castle Courtyard', 'in a tree'), - 'Wooden Bridge Tree': (0x11, False, '', 'Wooden Bridge Area', 'in a tree'), - 'Eastern Palace Tree': (0x12, True, '', 'Eastern Palace Area', 'in a tree'), - 'Flute Boy South Tree': (0x13, True, '', 'Flute Boy Area', 'in a tree'), - 'Flute Boy East Tree': (0x14, True, '', 'Flute Boy Area', 'in a tree'), - 'Central Bonk Rocks Tree': (0x15, False, '', 'Central Bonk Rocks Area', 'in a tree'), - 'Tree Line Tree 2': (0x16, True, '', 'Tree Line Area', 'in a tree'), - 'Tree Line Tree 4': (0x17, True, '', 'Tree Line Area', 'in a tree'), - 'Flute Boy Approach South Tree': (0x18, False, '', 'Flute Boy Approach Area', 'in a tree'), - 'Flute Boy Approach North Tree': (0x19, False, '', 'Flute Boy Approach Area', 'in a tree'), - 'Dark Lumberjack Tree': (0x1a, False, '', 'Dark Lumberjack Area', 'in a tree'), - 'Dark Fortune Bonk Rocks (Drop 1)': (0x1b, False, '', 'Dark Fortune Area', 'encased in stone'), - 'Dark Fortune Bonk Rocks (Drop 2)': (0x1c, False, '', 'Dark Fortune Area', 'encased in stone'), - 'Dark Graveyard West Bonk Rocks': (0x1d, False, '', 'Dark Graveyard Area', 'encased in stone'), - 'Dark Graveyard North Bonk Rocks': (0x1e, False, '', 'Dark Graveyard North', 'encased in stone'), - 'Dark Graveyard Tomb Bonk Rocks': (0x1f, False, '', 'Dark Graveyard North', 'encased in stone'), - 'Qirn Jump West Tree': (0x20, False, '', 'Qirn Jump Area', 'in a tree'), - 'Qirn Jump East Tree': (0x21, False, '', 'Qirn Jump East Bank', 'in a tree'), - 'Dark Witch Tree': (0x22, False, '', 'Dark Witch Area', 'in a tree'), - 'Pyramid Tree': (0x23, False, '', 'Pyramid Area', 'in a tree'), - 'Palace of Darkness Tree': (0x24, False, '', 'Palace of Darkness Area', 'in a tree'), - 'Dark Tree Line Tree 2': (0x25, False, '', 'Dark Tree Line Area', 'in a tree'), - 'Dark Tree Line Tree 3': (0x26, False, '', 'Dark Tree Line Area', 'in a tree'), - 'Dark Tree Line Tree 4': (0x27, False, '', 'Dark Tree Line Area', 'in a tree'), - 'Hype Cave Statue': (0x28, False, '', 'Hype Cave Area', 'encased in stone') + 'Lost Woods Hideout Tree': (0x00, 0x10, False, '', 'Lost Woods East Area', 'in a tree'), + 'Death Mountain Bonk Rocks': (0x01, 0x10, False, '', 'East Death Mountain (Top East)', 'encased in stone'), + 'Mountain Entry Pull Tree': (0x02, 0x10, False, '', 'Mountain Entry Area', 'in a tree'), + 'Mountain Entry Southeast Tree': (0x03, 0x08, False, '', 'Mountain Entry Area', 'in a tree'), + 'Lost Woods Pass West Tree': (0x04, 0x10, False, '', 'Lost Woods Pass West Area', 'in a tree'), + 'Kakariko Portal Tree': (0x05, 0x08, False, '', 'Lost Woods Pass East Top Area', 'in a tree'), + 'Fortune Bonk Rocks': (0x06, 0x10, False, '', 'Kakariko Fortune Area', 'in a tree'), + 'Kakariko Pond Tree': (0x07, 0x10, True, '', 'Kakariko Pond Area', 'in a tree'), + 'Bonk Rocks Tree': (0x08, 0x10, True, '', 'Bonk Rock Ledge', 'in a tree'), + 'Sanctuary Tree': (0x09, 0x08, False, '', 'Sanctuary Area', 'in a tree'), + 'River Bend West Tree': (0x0a, 0x10, True, '', 'River Bend Area', 'in a tree'), + 'River Bend East Tree': (0x0b, 0x08, False, '', 'River Bend East Bank', 'in a tree'), + 'Blinds Hideout Tree': (0x0c, 0x10, False, '', 'Kakariko Area', 'in a tree'), + 'Kakariko Welcome Tree': (0x0d, 0x08, False, '', 'Kakariko Area', 'in a tree'), + 'Forgotten Forest Southwest Tree': (0x0e, 0x10, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Forgotten Forest Central Tree': (0x0f, 0x08, False, '', 'Forgotten Forest Area', 'in a tree'), + #'Forgotten Forest Southeast Tree': (0x??, 0x04, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Hyrule Castle Tree': (0x10, 0x10, False, '', 'Hyrule Castle Courtyard', 'in a tree'), + 'Wooden Bridge Tree': (0x11, 0x10, False, '', 'Wooden Bridge Area', 'in a tree'), + 'Eastern Palace Tree': (0x12, 0x10, True, '', 'Eastern Palace Area', 'in a tree'), + 'Flute Boy South Tree': (0x13, 0x10, True, '', 'Flute Boy Area', 'in a tree'), + 'Flute Boy East Tree': (0x14, 0x08, True, '', 'Flute Boy Area', 'in a tree'), + 'Central Bonk Rocks Tree': (0x15, 0x10, False, '', 'Central Bonk Rocks Area', 'in a tree'), + 'Tree Line Tree 2': (0x16, 0x10, True, '', 'Tree Line Area', 'in a tree'), + 'Tree Line Tree 4': (0x17, 0x08, True, '', 'Tree Line Area', 'in a tree'), + 'Flute Boy Approach South Tree': (0x18, 0x10, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Flute Boy Approach North Tree': (0x19, 0x08, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Dark Lumberjack Tree': (0x1a, 0x10, False, '', 'Dark Lumberjack Area', 'in a tree'), + 'Dark Fortune Bonk Rocks (Drop 1)': (0x1b, 0x10, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Fortune Bonk Rocks (Drop 2)': (0x1c, 0x08, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Graveyard West Bonk Rocks': (0x1d, 0x10, False, '', 'Dark Graveyard Area', 'encased in stone'), + 'Dark Graveyard North Bonk Rocks': (0x1e, 0x08, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Dark Graveyard Tomb Bonk Rocks': (0x1f, 0x04, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Qirn Jump West Tree': (0x20, 0x10, False, '', 'Qirn Jump Area', 'in a tree'), + 'Qirn Jump East Tree': (0x21, 0x08, False, '', 'Qirn Jump East Bank', 'in a tree'), + 'Dark Witch Tree': (0x22, 0x10, False, '', 'Dark Witch Area', 'in a tree'), + 'Pyramid Tree': (0x23, 0x10, False, '', 'Pyramid Area', 'in a tree'), + 'Palace of Darkness Tree': (0x24, 0x10, False, '', 'Palace of Darkness Area', 'in a tree'), + 'Dark Tree Line Tree 2': (0x25, 0x10, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 3': (0x26, 0x08, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 4': (0x27, 0x04, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Hype Cave Statue': (0x28, 0x10, False, '', 'Hype Cave Area', 'encased in stone') } +bonk_table_by_location_id = {0x153B00+(data[0]*6)+3: name for name, data in bonk_prize_table.items()} +bonk_table_by_location = {y: x for x, y in bonk_table_by_location_id.items()} + # (room_id, type, shopkeeper, custom, locked, [items]) # item = (item, price, max=0, replacement=None, replacement_price=0) @@ -1662,5 +1665,7 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} lookup_id_to_name.update(shop_table_by_location_id) +lookup_id_to_name.update(bonk_table_by_location_id) lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int} lookup_name_to_id.update(shop_table_by_location) +lookup_name_to_id.update(bonk_table_by_location) diff --git a/Rules.py b/Rules.py index 9619c897..ffa8374f 100644 --- a/Rules.py +++ b/Rules.py @@ -827,7 +827,7 @@ def default_rules(world, player): if world.shuffle_bonk_drops[player]: if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld from Regions import bonk_prize_table - for location_name, (_, aga_required, _, _, _) in bonk_prize_table.items(): + for location_name, (_, _, aga_required, _, _, _) in bonk_prize_table.items(): loc = world.get_location(location_name, player) set_rule(loc, lambda state: (state.can_collect_bonkdrops(player)) and (not aga_required or state.has_beaten_aga(player))) add_bunny_rule(loc, player) From 3983bf347fbf16af032331d7fc4291f291d601b4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 20:57:58 -0500 Subject: [PATCH 219/293] Fixed Namespace error in Mystery with Bonk Drop option --- Mystery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index 60a3f064..1a02d10d 100644 --- a/Mystery.py +++ b/Mystery.py @@ -174,7 +174,7 @@ def roll_settings(weights): ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on' overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' - ret.shuffle_bonk_drops = get_choice('bonk_drops') == 'on' + ret.bonk_drops = get_choice('bonk_drops') == 'on' entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' overworld_map = get_choice('overworld_map') From e1cc74c05c9a53dac751ee1a67043c3658b7e0e2 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 21:27:40 -0500 Subject: [PATCH 220/293] Suppressing meta output if outputting spoiler --- Main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 2ac286bf..d3d0b3d3 100644 --- a/Main.py +++ b/Main.py @@ -160,7 +160,7 @@ def main(args, seed=None, fish=None): if args.create_spoiler 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: + if args.mystery and not (args.suppress_meta or args.create_spoiler): world.spoiler.mystery_meta_to_file(output_path(f'{outfilebase}_meta.txt')) for player in range(1, world.players + 1): @@ -359,7 +359,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) - if args.mystery and not args.suppress_meta: + 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: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) From 79448be9d4c7c756af60c67c101188c8322a8632 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 21:35:47 -0500 Subject: [PATCH 221/293] Fixed error with pot rule in OWR accessibility check --- Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index ffa8374f..36bbfafe 100644 --- a/Rules.py +++ b/Rules.py @@ -356,7 +356,7 @@ def global_rules(world, player): # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) loc = world.get_location('Misery Mire - Spikes Pot Key', player) - if loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area + if loc.pot is not None and loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) From b877ddb4d74a6e535d14c4d3702cd4c241a573c9 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Jul 2022 21:49:28 -0500 Subject: [PATCH 222/293] Making file output prefix always OR --- Main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Main.py b/Main.py index d3d0b3d3..6241a830 100644 --- a/Main.py +++ b/Main.py @@ -139,10 +139,7 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') - if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or world.owWhirlpoolShuffle[1] or world.owFluteShuffle[1] != 'vanilla' or str(args.outputname).startswith('M'): - outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' - else: - outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' + outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] From 4267c2dde5f4cb01c87fb3c3c7c47e5e328e2c57 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 25 Jul 2022 12:13:50 -0600 Subject: [PATCH 223/293] Minor bug in intensity 3, pottery setting not drop shuffle for Desert Tiles 2. --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 0d36a2b0..ecb1b503 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -380,7 +380,7 @@ def choose_portals(world, player): if world.doorShuffle[player] in ['basic', 'crossed']: cross_flag = world.doorShuffle[player] == 'crossed' # key drops allow the big key in the right place in Desert Tiles 2 - bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player] + bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) From ce269daa88f8bce50b56b2ff78711e8901ff7279 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 26 Jul 2022 21:06:09 -0500 Subject: [PATCH 224/293] Improvements to Bonk Drop Shuffle - Trees revert color when collected - Fixed issue with sprites getting stuck in rocks --- Rom.py | 2 +- asm/owrando.asm | 6 +++--- data/base2current.bps | Bin 104010 -> 104312 bytes 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Rom.py b/Rom.py index 0acf2ad8..cfdfdb02 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '528840ac74c94330e8bb70ce6d96161e' +RANDOMIZERBASEHASH = 'ae56110fe1228423c9d6a6cee361f7ac' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 7793ce2d..b5dc88f1 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -1340,9 +1340,9 @@ db $2e, $b4, $08, $b0, $00, $20 db $32, $29, $10, $42, $00, $20 db $32, $9a, $08, $b2, $00, $20 db $42, $66, $10, $b2, $00, $20 -db $51, $08, $10, $b2, $00, $00 -db $51, $09, $08, $b2, $00, $00 -db $54, $b5, $10, $27, $00, $00 +db $51, $08, $10, $b2, $00, $04 +db $51, $09, $08, $b2, $00, $04 +db $54, $b5, $10, $27, $00, $14 db $54, $ef, $08, $b2, $00, $08 db $54, $b9, $04, $36, $00, $00 db $55, $aa, $10, $b0, $00, $20 diff --git a/data/base2current.bps b/data/base2current.bps index 795be7ea926e1e80dc6fbde6d2e386a51d5c9775..3bfea7f1aee535c575f86e68eed66a3a9828780c 100644 GIT binary patch delta 6638 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 MzPVLV@~-*60Ce?vyZ`_I delta 6421 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 z Date: Tue, 26 Jul 2022 22:57:42 -0500 Subject: [PATCH 225/293] Fixed Aga rules on Bonk Locations? --- Rules.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index 36bbfafe..7e020507 100644 --- a/Rules.py +++ b/Rules.py @@ -829,7 +829,10 @@ def default_rules(world, player): from Regions import bonk_prize_table for location_name, (_, _, aga_required, _, _, _) in bonk_prize_table.items(): loc = world.get_location(location_name, player) - set_rule(loc, lambda state: (state.can_collect_bonkdrops(player)) and (not aga_required or state.has_beaten_aga(player))) + if not aga_required: + set_rule(loc, lambda state: state.can_collect_bonkdrops(player)) + else: + set_rule(loc, lambda state: state.can_collect_bonkdrops(player) and state.has_beaten_aga(player)) add_bunny_rule(loc, player) # Entrance Access From fb064b595033b99a9fe53d30827ee96e81ea1438 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Jul 2022 08:24:34 -0500 Subject: [PATCH 226/293] Guarantee Big Magic on non-Aga Bonk Location --- Fill.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index 80da0109..9633da49 100644 --- a/Fill.py +++ b/Fill.py @@ -364,10 +364,11 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None pot_item_pool[player].append(item) break + from Regions import bonk_prize_table for player, magic_pool in pot_item_pool.items(): world.itempool.remove(magic_pool[0]) - pot_locations = [location for location in fill_locations - if location.type == LocationType.Bonk and location.player == player] + pot_locations = [location for location in fill_locations if location.player == player + and location.name in [n for n, (_, _, aga, _, _, _) in bonk_prize_table.items() if not aga]] pot_locations = filter_pot_locations(pot_locations, world) fast_fill_helper(world, magic_pool, pot_locations) pots_used = True From 5ded3be2764bbe5cdb68deeeba6735654d96c9d7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Jul 2022 18:01:56 -0500 Subject: [PATCH 227/293] Rename Drop Shuffle to Enemy Drop Shuffle in spoiler --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 4a484834..ab4d536e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3141,7 +3141,7 @@ class Spoiler(object): outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][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('Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['dropshuffle'][player])) + outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['dropshuffle'][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('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) From ceff7688c4ee7fa739a86cf1b0988064d4771bdf Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Jul 2022 18:02:42 -0500 Subject: [PATCH 228/293] Fixed issue with bonk drops getting enabled in non-bonk drop --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index cfdfdb02..9ad28cab 100644 --- a/Rom.py +++ b/Rom.py @@ -792,7 +792,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) # patch bonk prizes - if world.shuffle_bonk_drops: + if world.shuffle_bonk_drops[player]: bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, From abdf4cd2588906353118de59119117959d1ffab8 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Jul 2022 16:05:00 -0500 Subject: [PATCH 229/293] ROM Fixes - Fixed Aga2 check on Ganon vulnerability - Fixed disappearing mirror portals in Inverted+CrossedOWR --- Rom.py | 2 +- asm/owrando.asm | 4 +++- data/base2current.bps | Bin 104312 -> 104324 bytes 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 9ad28cab..37962bc7 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'ae56110fe1228423c9d6a6cee361f7ac' +RANDOMIZERBASEHASH = 'c80605a079983f1a9dac37273e2b377a' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index b5dc88f1..b1c1ebdf 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -271,7 +271,9 @@ OWMirrorSpriteRestore: OWLightWorldOrCrossed: { lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq + - lda #$00 : rtl + lda.l InvertedMode : beq + + lda #$40 : rtl + + lda #$00 : rtl + jsl OWWorldCheck : rtl } diff --git a/data/base2current.bps b/data/base2current.bps index 3bfea7f1aee535c575f86e68eed66a3a9828780c..d51dc56eb7dc07a6d5483caeaf7dc404c89f27ce 100644 GIT binary patch delta 313 zcmV-90mlCLt_Fm!2C!iP1P0~#8na~q^#%bolUoQj0c*382#Ev%dy^au#TiG5s_H<9 zog$B$pdtd@q?R2=n(Gx-l8Gmi`VB4toU^T9V3+9XZ@RtE0-Mviamzg>NJs4De1Av!tEtH*)Gf;o4YiMYu z3O=DXpDCB_Isv{0p8=f}fCHDYI{_vF8v(c3I{~Kx1)ns54S<)1J^>{Gt+%W`0mN7c LV#=p3rzIpNYR-K$ delta 301 zcmV+|0n+}2uLk(82C!iP1ij}%7_(&o^#%bflUoQj0b;X~2#Ev%a+4el#Th<{s_H<9 zog$B$pdtd@q?R2=n(Gx-l8GLZ`VB4tld~xfR=Ew9ZBLzz@Y;aZn`T6rlZ?l40|pp^ zvdC(152*vG0Ba&Vh^lL^1(V<5mxMS0ClP3YrSJ@;^2tEZ2B`s)s;?E7y*L430T`Db zIRRDyNSAgw0YCu>m$x|qAR%#!%4o^3&;_Y^OgW3mLeK{ySgi(3Qj5tn&<3g57)&{r z_&EWh3*C$Q@RtE0-MviRm!&!ZJs3iM1Av!bEtH*)Gf;o4YiMYu3O=D*pF5ZOIsv`~ zqkozafS0~I0VV<;1GnZo0jB~6n_z$ufC-n9J^>{Gs<*g40mN7cU4gdVpqBo^rz3mE From e218b8dc97de3153e91407d3fb85fd91742788dd Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Jul 2022 19:52:32 -0500 Subject: [PATCH 230/293] Reserving bonk drop slot for Murahdahla tree --- Regions.py | 52 +++++++++++++++++++++--------------------- Rom.py | 2 +- asm/owrando.asm | 45 ++++++++++++++++++++++++++++++++++-- data/base2current.bps | Bin 104324 -> 104324 bytes 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/Regions.py b/Regions.py index 9e6f18ac..435f7622 100644 --- a/Regions.py +++ b/Regions.py @@ -1284,32 +1284,32 @@ bonk_prize_table = { 'Kakariko Welcome Tree': (0x0d, 0x08, False, '', 'Kakariko Area', 'in a tree'), 'Forgotten Forest Southwest Tree': (0x0e, 0x10, False, '', 'Forgotten Forest Area', 'in a tree'), 'Forgotten Forest Central Tree': (0x0f, 0x08, False, '', 'Forgotten Forest Area', 'in a tree'), - #'Forgotten Forest Southeast Tree': (0x??, 0x04, False, '', 'Forgotten Forest Area', 'in a tree'), - 'Hyrule Castle Tree': (0x10, 0x10, False, '', 'Hyrule Castle Courtyard', 'in a tree'), - 'Wooden Bridge Tree': (0x11, 0x10, False, '', 'Wooden Bridge Area', 'in a tree'), - 'Eastern Palace Tree': (0x12, 0x10, True, '', 'Eastern Palace Area', 'in a tree'), - 'Flute Boy South Tree': (0x13, 0x10, True, '', 'Flute Boy Area', 'in a tree'), - 'Flute Boy East Tree': (0x14, 0x08, True, '', 'Flute Boy Area', 'in a tree'), - 'Central Bonk Rocks Tree': (0x15, 0x10, False, '', 'Central Bonk Rocks Area', 'in a tree'), - 'Tree Line Tree 2': (0x16, 0x10, True, '', 'Tree Line Area', 'in a tree'), - 'Tree Line Tree 4': (0x17, 0x08, True, '', 'Tree Line Area', 'in a tree'), - 'Flute Boy Approach South Tree': (0x18, 0x10, False, '', 'Flute Boy Approach Area', 'in a tree'), - 'Flute Boy Approach North Tree': (0x19, 0x08, False, '', 'Flute Boy Approach Area', 'in a tree'), - 'Dark Lumberjack Tree': (0x1a, 0x10, False, '', 'Dark Lumberjack Area', 'in a tree'), - 'Dark Fortune Bonk Rocks (Drop 1)': (0x1b, 0x10, False, '', 'Dark Fortune Area', 'encased in stone'), - 'Dark Fortune Bonk Rocks (Drop 2)': (0x1c, 0x08, False, '', 'Dark Fortune Area', 'encased in stone'), - 'Dark Graveyard West Bonk Rocks': (0x1d, 0x10, False, '', 'Dark Graveyard Area', 'encased in stone'), - 'Dark Graveyard North Bonk Rocks': (0x1e, 0x08, False, '', 'Dark Graveyard North', 'encased in stone'), - 'Dark Graveyard Tomb Bonk Rocks': (0x1f, 0x04, False, '', 'Dark Graveyard North', 'encased in stone'), - 'Qirn Jump West Tree': (0x20, 0x10, False, '', 'Qirn Jump Area', 'in a tree'), - 'Qirn Jump East Tree': (0x21, 0x08, False, '', 'Qirn Jump East Bank', 'in a tree'), - 'Dark Witch Tree': (0x22, 0x10, False, '', 'Dark Witch Area', 'in a tree'), - 'Pyramid Tree': (0x23, 0x10, False, '', 'Pyramid Area', 'in a tree'), - 'Palace of Darkness Tree': (0x24, 0x10, False, '', 'Palace of Darkness Area', 'in a tree'), - 'Dark Tree Line Tree 2': (0x25, 0x10, False, '', 'Dark Tree Line Area', 'in a tree'), - 'Dark Tree Line Tree 3': (0x26, 0x08, False, '', 'Dark Tree Line Area', 'in a tree'), - 'Dark Tree Line Tree 4': (0x27, 0x04, False, '', 'Dark Tree Line Area', 'in a tree'), - 'Hype Cave Statue': (0x28, 0x10, False, '', 'Hype Cave Area', 'encased in stone') + #'Forgotten Forest Southeast Tree': (0x10, 0x04, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Hyrule Castle Tree': (0x11, 0x10, False, '', 'Hyrule Castle Courtyard', 'in a tree'), + 'Wooden Bridge Tree': (0x12, 0x10, False, '', 'Wooden Bridge Area', 'in a tree'), + 'Eastern Palace Tree': (0x13, 0x10, True, '', 'Eastern Palace Area', 'in a tree'), + 'Flute Boy South Tree': (0x14, 0x10, True, '', 'Flute Boy Area', 'in a tree'), + 'Flute Boy East Tree': (0x15, 0x08, True, '', 'Flute Boy Area', 'in a tree'), + 'Central Bonk Rocks Tree': (0x16, 0x10, False, '', 'Central Bonk Rocks Area', 'in a tree'), + 'Tree Line Tree 2': (0x17, 0x10, True, '', 'Tree Line Area', 'in a tree'), + 'Tree Line Tree 4': (0x18, 0x08, True, '', 'Tree Line Area', 'in a tree'), + 'Flute Boy Approach South Tree': (0x19, 0x10, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Flute Boy Approach North Tree': (0x1a, 0x08, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Dark Lumberjack Tree': (0x1b, 0x10, False, '', 'Dark Lumberjack Area', 'in a tree'), + 'Dark Fortune Bonk Rocks (Drop 1)': (0x1c, 0x10, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Fortune Bonk Rocks (Drop 2)': (0x1d, 0x08, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Graveyard West Bonk Rocks': (0x1e, 0x10, False, '', 'Dark Graveyard Area', 'encased in stone'), + 'Dark Graveyard North Bonk Rocks': (0x1f, 0x08, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Dark Graveyard Tomb Bonk Rocks': (0x20, 0x04, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Qirn Jump West Tree': (0x21, 0x10, False, '', 'Qirn Jump Area', 'in a tree'), + 'Qirn Jump East Tree': (0x22, 0x08, False, '', 'Qirn Jump East Bank', 'in a tree'), + 'Dark Witch Tree': (0x23, 0x10, False, '', 'Dark Witch Area', 'in a tree'), + 'Pyramid Tree': (0x24, 0x10, False, '', 'Pyramid Area', 'in a tree'), + 'Palace of Darkness Tree': (0x25, 0x10, False, '', 'Palace of Darkness Area', 'in a tree'), + 'Dark Tree Line Tree 2': (0x26, 0x10, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 3': (0x27, 0x08, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 4': (0x28, 0x04, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Hype Cave Statue': (0x29, 0x10, False, '', 'Hype Cave Area', 'encased in stone') } bonk_table_by_location_id = {0x153B00+(data[0]*6)+3: name for name, data in bonk_prize_table.items()} diff --git a/Rom.py b/Rom.py index 37962bc7..68768420 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'c80605a079983f1a9dac37273e2b377a' +RANDOMIZERBASEHASH = '61e3137ae471ed8772deb7f84cc85fb9' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index b1c1ebdf..07ab9f55 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -385,7 +385,7 @@ OWBonkDrops: ; loop thru rando bonk table to find match PHB : PHK : PLB LDA.b $8A - LDX.b #(40*6) ; 40 bonk items, 6 bytes each + LDX.b #(41*6) ; 41 bonk items, 6 bytes each - CMP.w OWBonkPrizeData,X : BNE + INX LDA.w $0D10,Y : LSR A : LSR A : LSR A : LSR A @@ -1311,6 +1311,47 @@ db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0 +;================================================================================ +; Bonk Prize Data ($AABB00 - $AABBFB) +;-------------------------------------------------------------------------------- +; This table stores data relating to bonk locations for Bonk Drop Shuffle +; +; Example: We can use OWBonkPrizeTable[$09].loot to read what item is in the +; east tree on the Sanctuary screen +;-------------------------------------------------------------------------------- +; Search Criteria - The following two fields are used as a unique index +; .owid = OW screen ID +; .yx = Y & X coordinate data *see below* +; +; .flag = OW event flag bitmask +; .loot = Loot ID +; .mw_player = Multiworld player ID +; .vert_offset = Vertical offset, # of pixels the sprite moves up when activated +; +; .yx field is a combination of both the least significant digits of the Y and X +; coordinates of the static location of the sprite located in a bonk location. +; All sprites, when initialized, are aligned by a 16 pixel increment. +; The coordinate system in LTTP is handled by two bytes: +; (high) (low) +; - - - w w w w s s s s s s s s s +; w = world absolute coords, every screen is $200 pixels in each dimension +; s = local screen coords, coords relative to the bounds of the current screen +; Because of the 16 pixel alignment of sprites, the last four bits of the coords +; are unset. This leaves 5 bits remaining, we simply disregard the highest bit +; and then combine the Y and X coords together to be used as search criteria. +; This does open the possibility of a false positive match from 3 other coords +; on the same screen (15 on megatile screens) but there are no bonk sprites that +; have collision in this regard. +;-------------------------------------------------------------------------------- +struct OWBonkPrizeTable $AABB00 + .owid: skip 1 + .yx: skip 1 + .flag: skip 1 + .loot: skip 1 + .mw_player: skip 1 + .vert_offset: skip 1 +endstruct align 6 + org $aabb00 ;PC 153b00 OWBonkPrizeData: ; OWID YX Flag Item MW Offset @@ -1330,7 +1371,7 @@ db $18, $a8, $10, $b2, $00, $20 db $18, $36, $08, $35, $00, $20 db $1a, $8a, $10, $42, $00, $20 db $1a, $1d, $08, $b2, $00, $20 -;db $1a, $77, $04, $35, $00, $20 ; pre aga ONLY ; hijacked murahdahla bonk tree +db $ff, $77, $04, $35, $00, $20 ; pre aga ONLY ; hijacked murahdahla bonk tree db $1b, $46, $10, $b1, $00, $10 db $1d, $6b, $10, $b1, $00, $20 db $1e, $72, $10, $b2, $00, $20 diff --git a/data/base2current.bps b/data/base2current.bps index d51dc56eb7dc07a6d5483caeaf7dc404c89f27ce..c56129a9f3e943873ee428c38f821efcc1c2366b 100644 GIT binary patch delta 9079 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 Date: Fri, 29 Jul 2022 12:39:00 -0500 Subject: [PATCH 231/293] Fixed Multiworld support for Bonk Drop shuffle --- Fill.py | 16 ++++++++-------- Regions.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Fill.py b/Fill.py index 9633da49..75637093 100644 --- a/Fill.py +++ b/Fill.py @@ -360,18 +360,18 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None for player in range(1, world.players + 1): if world.shuffle_bonk_drops[player]: for item in world.itempool: - if item.name in ['Big Magic']: + if item.name in ['Big Magic'] and item.player == player: pot_item_pool[player].append(item) break - from Regions import bonk_prize_table for player, magic_pool in pot_item_pool.items(): - world.itempool.remove(magic_pool[0]) - pot_locations = [location for location in fill_locations if location.player == player - and location.name in [n for n, (_, _, aga, _, _, _) in bonk_prize_table.items() if not aga]] - pot_locations = filter_pot_locations(pot_locations, world) - fast_fill_helper(world, magic_pool, pot_locations) - pots_used = True + if len(magic_pool) > 0: + world.itempool.remove(magic_pool[0]) + pot_locations = [location for location in fill_locations if location.player == player + and location.name in [n for n, (_, _, aga, _, _, _) in bonk_prize_table.items() if not aga]] + pot_locations = filter_pot_locations(pot_locations, world) + fast_fill_helper(world, magic_pool, pot_locations) + pots_used = True if pots_used: fill_locations = world.get_unfilled_locations() diff --git a/Regions.py b/Regions.py index 435f7622..a37b7da5 100644 --- a/Regions.py +++ b/Regions.py @@ -1312,7 +1312,7 @@ bonk_prize_table = { 'Hype Cave Statue': (0x29, 0x10, False, '', 'Hype Cave Area', 'encased in stone') } -bonk_table_by_location_id = {0x153B00+(data[0]*6)+3: name for name, data in bonk_prize_table.items()} +bonk_table_by_location_id = {0x2ABB00+(data[0]*6)+3: name for name, data in bonk_prize_table.items()} bonk_table_by_location = {y: x for x, y in bonk_table_by_location_id.items()} From 1e0a0461519e3b1368d34f4e667f6617986d180b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 29 Jul 2022 16:41:29 -0500 Subject: [PATCH 232/293] Fixed 4 digits collection rate in credits issue --- Rom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 68768420..ae8e8bfa 100644 --- a/Rom.py +++ b/Rom.py @@ -1072,7 +1072,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(cr_pc+0x1f, thousands_bot) # modify stat config stat_address = 0x23B969 - stat_pc = snes_to_pc(stat_address) + owr_difference = 0x26 # can't remember why there is a difference between DR fork + stat_pc = snes_to_pc(stat_address - owr_difference) rom.write_byte(stat_pc, 0xa9) # change to pos 21 (from b1) rom.write_byte(stat_pc+2, 0xc0) # change to 12 bits (from a0) rom.write_byte(stat_pc+3, 0x80) # change to four digits (from 60) From eca58ebccaec2502ab6a22a645bef96022477730 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 29 Jul 2022 16:45:21 -0500 Subject: [PATCH 233/293] Adjusted sprite alignment when coming from a chest --- Rom.py | 2 +- data/base2current.bps | Bin 104324 -> 104326 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index ae8e8bfa..1f10b8e3 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '61e3137ae471ed8772deb7f84cc85fb9' +RANDOMIZERBASEHASH = '0574a782e225a87b90637db0847c5ae0' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index c56129a9f3e943873ee428c38f821efcc1c2366b..b48472f343c2e495ca98cb4f09ae31edac2e3b87 100644 GIT binary patch delta 43 zcmV+`0M!44uLg#%2C!iP0~*ccvt$8TLk5WhDfq8dgTX|%!9)SOQV8^|<-`qez2b)m B6M+B# delta 41 xcmZo$&(^Y@Z9^g>v)0+Gn^PDgomm>$w?{S~cG-T|g>iQf$47>=`}^uzY5`k`5qAIp From a36d65ef8115cccea5a536922b9e04afc4225242 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 29 Jul 2022 21:29:32 -0500 Subject: [PATCH 234/293] Adding sprite patch for Apples so they don't prevent kill rooms from opening --- Rom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 1f10b8e3..688c0274 100644 --- a/Rom.py +++ b/Rom.py @@ -1667,10 +1667,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # sprite patches - rom.write_byte(snes_to_pc(0x0db7d1), 0x03) # patch apple sprites to not permadeatch like enemies + rom.write_byte(snes_to_pc(0x0DB7D1), 0x03) # patch apple sprites to not permadeatch like enemies + rom.write_byte(snes_to_pc(0x0DB4F8), 0x40) # patch apples to not prevent kill rooms from opening if world.shuffle_bonk_drops[player]: # warning, this temporary patch might cause fairies to respawn differently?, limiting this to bonk drop mode only - rom.write_byte(snes_to_pc(0x0db808), 0x03) # patch fairies sprites to not permadeatch like enemies + rom.write_byte(snes_to_pc(0x0DB808), 0x03) # patch fairies sprites to not permadeath like enemies # allow smith into multi-entrance caves in appropriate shuffles if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): From 383a2a274de253f4936e919bd3da076875a94168 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 29 Jul 2022 21:33:10 -0500 Subject: [PATCH 235/293] Catching infinite loop at balance_money_progression --- Fill.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Fill.py b/Fill.py index 75637093..daa6f6bd 100644 --- a/Fill.py +++ b/Fill.py @@ -830,7 +830,12 @@ def balance_money_progression(world): return False done = False + attempts = world.players * 20 + 20 while not done: + attempts -= 1 + if attempts < 0: + from DungeonGenerator import GenerationException + raise GenerationException(f'Infinite loop detected at "balance_money_progression"') sphere_costs = {player: 0 for player in range(1, world.players+1)} locked_by_money = {player: set() for player in range(1, world.players+1)} sphere_locations = get_sphere_locations(state, unchecked_locations) From a9fa2a326f8e1c8aa21b0106ae99ebf3837f04e3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 29 Jul 2022 21:35:27 -0500 Subject: [PATCH 236/293] Scaling infinite loop check to number of players --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index ab4d536e..551d4957 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1606,7 +1606,7 @@ class Region(object): def can_reach(self, state): from Utils import stack_size3a from DungeonGenerator import GenerationException - if stack_size3a() > 500: + if stack_size3a() > self.world.players * 500: raise GenerationException(f'Infinite loop detected for "{self.name}" located at \'Region.can_reach\'') if state.stale[self.player]: From 8721858e2c790def7757dd2ea0b112e503a79953 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 29 Jul 2022 22:37:38 -0500 Subject: [PATCH 237/293] Version bump 0.2.9.0 --- CHANGELOG.md | 7 +++++++ OverworldShuffle.py | 2 +- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aef17765..fe3f74df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 0.2.9.0 +- Added Bonk Drop Shuffle +- Fixed disappearing mirror portal issue in Inverted+Crossed OWR +- Fixed 4-digit collection rate in credits +- Fixed Ganon vulnerability to reference Aga2 boss flag rather than pyramid hole +- Fixed issue with pre-opened pyramid when not expected + ### 0.2.8.0 - ~Merged DR v1.0.1.0 - Pottery options, BPS support, MSU Resume, Collection Rate Counter~ - Various improvements to increase generation success rate and reduce generation time diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 5a940200..f5812ffc 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.8.0' +version_number = '0.2.9.0' version_branch = '-u' __version__ = '%s%s' % (version_number, version_branch) diff --git a/README.md b/README.md index 9dbb35fe..c6e5187c 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Alternatively, run ```Gui.py``` for a simple graphical user interface. # Settings -Only extra settings are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) +Only extra settings added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) ## Overworld Layout Shuffle (--ow_shuffle) @@ -136,6 +136,37 @@ New flute spots are chosen at random, with restrictions that limit the promixity New flute spots are chosen at random with minimum bias. +## Bonk Drop Shuffle (--bonk_drops) + +This adds 41 new item locations to the game. These bonk locations are limited to the ones that drop a static item in the vanilla game. + +- Bonk Locations consist of some trees, rocks, and statues + - 33 Trees + - 8 of the tree locations require Agahnim to be defeated to access the item + - 6 Rocks + - 1 of the rocks drops 2 items + - 1 Statue +- Bonk locations can be collected by bonking into them with the Pegasus Boots or using the Quake Medallion +- One of the bonk locations are guaranteed to have a full magic decanter +- Some of the drops can be farmed repeatedly, but only increments the collection rate once +- All of the bonk trees have been given an alternate color (and all non-bonk trees are reverted to normal tree color) + - 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 +- Since Fairies and Apples are new items that can appear in plain sight, they don't have a proper graphic for them yet. For now, they show up as Power Stars + +Future Note: This does NOT include the Good Bee (Cold Bee) Cave Statue...yet. In the future, this could be an additional item location. + +#### Items Added To Pool: +- 15 Fairies +- 8 Apples +- 6 Bee Traps +- 3 Red Rupees +- 3 Blue Rupees +- 2 Single Bomb +- 2 Small Hearts +- 1 Large Magic Decanter +- 1 8x Bomb Pack + ## New Goal Options (--goal) ### Trinity @@ -216,3 +247,9 @@ This gives each OW tile a random chance to be swapped to the opposite world ``` For randomizing the flute spots around the overworld + +``` +--bonk_drops +``` + +This extends the item pool to bonk locations and makes them additional item locations From 578c4d8065015e10822d1d5fed223f9c28b5fc64 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 30 Jul 2022 09:35:25 -0500 Subject: [PATCH 238/293] Lite/Lean now considers Cave Pottery options --- EntranceShuffle.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index dc48acdc..b15a7438 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -73,7 +73,7 @@ def link_entrances(world, player): # if we do not shuffle, set default connections if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - for entrancename, exitname in default_connections + drop_connections + default_item_connections + default_shop_connections: + for entrancename, exitname in default_connections + default_pot_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) @@ -341,7 +341,7 @@ def link_entrances(world, 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): + 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): 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) @@ -433,7 +433,7 @@ def link_entrances(world, 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): + 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): 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) @@ -1356,7 +1356,7 @@ def place_links_house(world, player, ignore_list=[]): else: links_house_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']: - 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 []))))[0]] + 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 []))))[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] @@ -1404,7 +1404,7 @@ def place_blacksmith(world, links_house, player): 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 []))))[0]] + 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 []))))[0]] assert len(blacksmith_doors), 'No valid candidates to place Blacksmiths Hut' blacksmith_hut = random.choice(blacksmith_doors) @@ -1472,7 +1472,7 @@ def junk_fill_inaccessible(world, player): 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))[1] if e in exit_pool] + junk_locations = [e for e in list(zip(*(default_connections + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections))))[1] if e in exit_pool] random.shuffle(junk_locations) for entrance in inaccessible_entrances: connect_entrance(world, entrance, junk_locations.pop(), player) @@ -2089,41 +2089,43 @@ mandatory_connections = [('Old Man S&Q', 'Old Man House'), ] # non-shuffled entrance links -default_connections = [('Lumberjack House', 'Lumberjack House'), - ('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), +default_connections = [('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), ('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'), ('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'), ('Light Hype Fairy', 'Swamp Healer Fairy'), ('Desert Fairy', 'Desert Healer Fairy'), ('Lost Woods Gamble', 'Lost Woods Gamble'), ('Fortune Teller (Light)', 'Fortune Teller (Light)'), - ('Snitch Lady (East)', 'Snitch Lady (East)'), - ('Snitch Lady (West)', 'Snitch Lady (West)'), ('Bush Covered House', 'Bush Covered House'), - ('Tavern (Front)', 'Tavern (Front)'), - ('Light World Bomb Hut', 'Light World Bomb Hut'), ('Long Fairy Cave', 'Long Fairy Cave'), # near East Light World Teleporter ('Good Bee Cave', 'Good Bee Cave'), - ('20 Rupee Cave', '20 Rupee Cave'), - ('50 Rupee Cave', '50 Rupee Cave'), ('Kakariko Gamble Game', 'Kakariko Gamble Game'), - ('Hookshot Fairy', 'Hookshot Fairy'), ('East Dark World Hint', 'East Dark World Hint'), - ('Palace of Darkness Hint', 'Palace of Darkness Hint'), ('Dark Lake Hylia Fairy', 'Dark Lake Hylia Healer Fairy'), ('Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Healer Fairy'), - ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave'), ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint'), ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), ('Archery Game', 'Archery Game'), - ('Dark Desert Hint', 'Dark Desert Hint'), ('Dark Desert Fairy', 'Dark Desert Healer Fairy'), ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'), ] +default_pot_connections = [('Lumberjack House', 'Lumberjack House'), + ('Snitch Lady (East)', 'Snitch Lady (East)'), + ('Snitch Lady (West)', 'Snitch Lady (West)'), + ('Tavern (Front)', 'Tavern (Front)'), + ('Light World Bomb Hut', 'Light World Bomb Hut'), + ('20 Rupee Cave', '20 Rupee Cave'), + ('50 Rupee Cave', '50 Rupee Cave'), + ('Hookshot Fairy', 'Hookshot Fairy'), + ('Palace of Darkness Hint', 'Palace of Darkness Hint'), + ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave'), + ('Dark Desert Hint', 'Dark Desert Hint') + ] + default_connector_connections = [('Old Man Cave (West)', 'Old Man Cave Exit (West)'), ('Old Man Cave (East)', 'Old Man Cave Exit (East)'), ('Old Man House (Bottom)', 'Old Man House Exit (Bottom)'), From 2c9efecd3835f01b5efad1f31cc49874a5b41531 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 31 Jul 2022 21:56:21 -0500 Subject: [PATCH 239/293] Ensure GT Bosses are unique in Unique Boss Shuffle --- Bosses.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Bosses.py b/Bosses.py index 53393d5f..a42b5176 100644 --- a/Bosses.py +++ b/Bosses.py @@ -202,12 +202,14 @@ def place_bosses(world, player): place_boss(boss, level, loc, loc_text, world, player) elif world.boss_shuffle[player] == 'unique': bosses = list(placeable_bosses) + gt_bosses = list() for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') try: if level: - boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)]) + boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level) and b not in gt_bosses]) + gt_bosses.append(boss) else: boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) bosses.remove(boss) From 57209296e8a5c76da16473c62bc4496bd3c6f625 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 31 Jul 2022 22:35:00 -0500 Subject: [PATCH 240/293] Necessary ROM Fixes - Changed Inverted MSU-1 to trigger opposite world track changes - Fixed mirror portal issue in Inverted - Fixed TR Peg Puzzle issue --- Rom.py | 2 +- asm/owrando.asm | 8 ++++---- data/base2current.bps | Bin 104326 -> 104342 bytes 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Rom.py b/Rom.py index 688c0274..63b5577c 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '0574a782e225a87b90637db0847c5ae0' +RANDOMIZERBASEHASH = '92a390672efafb652774c1514ac66c4b' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 07ab9f55..04ffd047 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -270,11 +270,11 @@ OWMirrorSpriteRestore: } OWLightWorldOrCrossed: { - lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq + + lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq ++ lda.l InvertedMode : beq + - lda #$40 : rtl - + lda #$00 : rtl - + jsl OWWorldCheck : rtl + lda #$40 + + rtl + ++ jsl OWWorldCheck : rtl } OWFluteCancel: diff --git a/data/base2current.bps b/data/base2current.bps index b48472f343c2e495ca98cb4f09ae31edac2e3b87..c6d73f6506521d127b1a55f7967369a005601735 100644 GIT binary patch delta 3836 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 delta 3828 zcmW+&c~}$Y)}NCFl0YDlT@=EIY=VM_ASwpj08#0uQm>{hir8Q*xOA~q!%P$U%U~J0=)F+HxS0nJ{Qs>Zx1%s|S-AHAqo~%AyqmA;BoG+6vARt(J?VHoIaI@t zgXoxee6sEe<6m{N=wL}DdX(bCli()2pA=#GiUTAs04 z&12o!&wRGWthi;*+j^i^M>-xe_id(grylhV-B9e-Db_q@wrQHq)>il^et*owRO=K0 zkD17QdeB7b&|D2c>E|`+NWy+UPy}$*{xqm9I-^OIcinJ%luJA`zPsr9{`r6EE9r+u z4gI{KEVY3iv?hPn7ekE#g{0IWeHe|T^eYigMQD7e+{NhXoHyaRJz0K=>wo7rMt{Aw z)a6M|=tuhuWvsH+&Y9QC_pj|^ETH?HL&o1^Y=};D^T}rPDCbQOqs6&0*lRD%y#}z+ zK0hy#n^HGZysJW$jmjIGga!0OetF9qPJefZg?Oc-K&-9*TCHzvX{3C}{7+3vRrV?6 zE@WPs%$@dHuleVt0`c8j3?(8Xe`6>fS!BQW`X_$yrM*^Fzzv*xlj%6iyA(ksx{V=u zdrmk#-J#?P1Z6?6!X8qV91JVb;=M&sYCo~}ZC_#d07J%KHj%0J2wdb9R=Yw+P+^o_ zTVJd*?%JYLBQ;fD#j1yO$51n3nsX#=WtzWsXy_|wKznG{ebkTsX`bmZgQ7L)eUxEd zz;|rabjj`2W_bkcvVV8>0R-N-$+U#Jqnp;2?7LmnSV|{W1c`xJJ*<=?Aeti9{|J`b2_Z~F0>2RQ79u0QcD zZ|qLAeei|V<@fE%6_+|ymfg@S6OlWdp|3O(mh972Kr?Bsq?=LOQB{AUjZax)ITz-h zZS4J`y*p9Wd#Wc=Q<3#$8(;IMUuS{!PUn)Z3asT_`Ck`UPq3spy{x;y+Swg_=BIpG zSNzTNZ}ohNHlZzNbOqMUJ;{AZXN>Dk)93yosox5$$Ica4XFGk)7g%?*RA3Fb@QU}L zdjr2!{C#osbc&|HYD2UB*a&IJ@<&6E*MDfPUUTvTuI{r0%Qmj=-wB1b8e6`7)uTZ# zUg#%y3(6g_c_poMkkXgvwvM4s+2@FJ{h;iq@Jn2~@wNHPi*F z><({U>`TI`ycJr*&GWvZ5X@4##3xu-e~DR6Jb4i z@1INvLXLlK6|GxAq2wLav{w){ZAW!|yRw-es#J9$761A5D)aOzGv-Ie?-~<(jdA=d zq)*#;OY=`H|BaQnxw1#S_`|dl zmu1h<$jb@xsqYRn^4xwc&fpT^88a==cNmw>8*E!LZ}33$Hp2>PajUay=to8}$?10S z?n$Zn&c`mk?0GLW&H02?9-%izQ+cLd*$Q-MbiV)Z*Un3Zk1Tlii}xs3e);+Hl4q>P zo~%JSb%N#92bT;GD|hv5fUGzL}{v@W_&1Oic!zukD?u`cjI{(Q9-$!)m2*r+PnLi32?#DP&H# zo=(RagHlDV`P{fAOm7I!{>2t;oR14VAT(vYr~dSq?g-n4VReassNq^hNA(+U@%9>| z07Hj=cSH*jJ4%$X=C(&DIkKigUurJJ$30;7*n#WW_=9!(@w;#D$KxX(G>UCAU!^n? zCt^=eh!YQ7$A0RXx+#k~e$E@f8J;j>%=-cQmZ)KMa9L`8d8{wmgBD?}C(I9A@B!VC zHk382)@S6G$BZbCc!X)#9^CPS$)Lb~0+<9#@f-mh%<#+*P(Lazy7|?M+AW@q`M$EQ zesW+qOg7rzG%NX!CVosG-S_?aNp3{s{n{!`JVQS-`20wLGW7>okI6ir`SEq z>FZ8&O5E8_se6$#(7n(Z;9AViaBgr(0+pGK7r%bk;^HeUv#4Yxm6(mgy!ceTYglQS zO_9cxgyy3ANlTSe?z*M4_)Iv_(pt ze0EqK(IJm;J+sg{A`34ml~SkWEfV%7M|$R znI!~Ox9nv_x1?tS2q|;Rk;vj^V=&J3hFqC2Q*R;MKDIWte>}onmo%sOXp2c@<51DA zD7+vOf94GZg5tf}9o;gcHy$H~EnYcyYL<7)Y=DY?zRx2KQ47@$gkq-C`7KNTNK`hOGJJ)Kl3xe8&eOIqRyi$QQzc<`N>x zLQqhpCzhA*_X50RtB^uMEcbiuzpWxs2~npTGk29qM)b?N9zDcG5;z(h2p1iH4t=fdBDY^yQ{+iG zqSks+c;|XNP71-?wdLR8R4K%B7H@DAOW_g(=BC%w4w=RgwkzoM!P<1^C~koT5n}hJ; z0H^>Dd?f%Tb5?J}+(0OSNc>J9WQbFyY1+f4Ydp0%dzWQ7J`aRkk*H|vUac=V-dMK5 z;T-{mz!&o?jY7PcgkzpaxlTki1mcJZ;Kvmb8b{g$IK+h{d^rl_%QobDQMK_sYNb=8 zuf+$A+pFjrq%oS1;Qh)I1h`h)GT4)OpIbAzeLbe9;UcM)uEFbRyhBT;nKeh#d(>1a z8B*D7^yh1ht0h|F0=zC7J`ED((5IOmb!VbmR)0v}+M{kSd^w6I;e;3nfpK_Y47+aS zjx90p0w5Ka#X(?@UqPjIm5H$JrinI9{YQlrCf$A>&T<@%gI~aN+Ui!CgHs(l%d>nh#xEE$pj_Tsn{m^W_k4Q52C za@yPj&Pey|k*1{+D-FtYmC0l)+Ty56fiw^XPpPSkPd3|yjtkS_1dOGHIAB(5EM<|a zEQE@jRcTfIKY#)kHl#uHn8_KfEpG3YC+t%2umG~!qOmg#)<6WFGy@7?71qoEc~+!Nn7P*boy4i^SIo^nugv?X`n=M4oV9+KmsRb|VnOxF4v)Om)y`?` zvp)ytWno@AG<)5v9&zJEvKyUw_wb2ym>s-a&86_>goBOxl~UO%$~cZ5`n`O0Yi5j2j(MYd|?&Fx+wXtawvr|Nj6>!fDC? From baaa6fcfc7ff1097ab05483cd525ed1657cb3d43 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 31 Jul 2022 22:55:36 -0500 Subject: [PATCH 241/293] Prevent unwanted 'allowed' Crossed OWR option --- BaseClasses.py | 2 +- Mystery.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 551d4957..e37b8d47 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3123,7 +3123,7 @@ class Spoiler(object): outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][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] not in ['none', 'allowed']: + 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('Swapped OW (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])) diff --git a/Mystery.py b/Mystery.py index 1a02d10d..e1be0eb7 100644 --- a/Mystery.py +++ b/Mystery.py @@ -168,7 +168,9 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' + valid_options = {'none', 'polar', 'grouped', 'limited', 'chaos'} ret.ow_crossed = get_choice('overworld_crossed') + ret.ow_crossed = ret.ow_crossed if ret.ow_crossed in valid_options else 'none' ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on' ret.ow_mixed = get_choice('overworld_swap') == 'on' ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on' From ae9ba04f248e4460471c5b3ccda660cf38a6ee59 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 31 Jul 2022 23:05:38 -0500 Subject: [PATCH 242/293] Version bump 0.2.9.1 --- CHANGELOG.md | 8 ++++++++ OverworldShuffle.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3f74df..1755913e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 0.2.9.1 +- Lite/Lean ER now includes Cave Pot locations with various Pottery options +- Changed Unique Boss Shuffle so that GT Bosses are unique amongst themselves +- Changed MSU-1 in Inverted to trigger DW2 track with Aga1 kill and LW2 with 7 crystals +- Fixed disappearing mirror portal issue in Inverted (Hopefully for good) +- Fixed issue with TR Peg Puzzle not spawning portal in some Mixed OWR scenarios +- Removed ability to roll Myserty with phantom Crossed OWR options + ### 0.2.9.0 - Added Bonk Drop Shuffle - Fixed disappearing mirror portal issue in Inverted+Crossed OWR diff --git a/OverworldShuffle.py b/OverworldShuffle.py index f5812ffc..1e819ae4 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,8 +6,10 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.9.0' +version_number = '0.2.9.1' +# branch indicator is intentionally different across branches version_branch = '-u' + __version__ = '%s%s' % (version_number, version_branch) def link_overworld(world, player): From 7fecbee3bb39cf1322a7f3ee2f4d9eebc60a107a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 1 Aug 2022 15:51:33 -0500 Subject: [PATCH 243/293] Removed 'allowed' Crossed OWR option --- README.md | 2 -- resources/app/cli/args.json | 1 - resources/app/cli/lang/en.json | 1 - resources/app/gui/lang/en.json | 1 - resources/app/gui/randomize/overworld/widgets.json | 1 - 5 files changed, 6 deletions(-) diff --git a/README.md b/README.md index c6e5187c..be396995 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,6 @@ OW Transitions are shuffled within each world separately. This allows OW connections to be shuffled cross-world. -'None (Allowed)' allows entrance connectors and whirlpools to result in cross-world behavior, but edge transitions will not. This isn't a recommended option. - Polar and Grouped both are guaranteed to result in two separated planes of tiles. To navigate to the other plane, you have the following methods: 1) Normal portals 2) Mirroring on DW tiles 3) Fluting to a LW tile that was previously unreachable Limited and Chaos are not bound to follow a two-plane framework. This means that it could be possible to travel on foot to every tile without entering a normal portal. diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index ebeedf53..d8166c48 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -148,7 +148,6 @@ "ow_crossed": { "choices": [ "none", - "allowed", "polar", "grouped", "limited", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 5d753a66..aabea6c0 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -217,7 +217,6 @@ "ow_crossed": [ "This allows cross-world connections to occur on the overworld.", "None: No transitions are cross-world connections.", - "Allowed: Only entrances/whirlpools can end up cross-world.", "Polar: Only used when Mixed is enabled. This retains original", " connections even when overworld tiles are swapped.", "Limited: Exactly nine transitions are randomly chosen as", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index b25f4233..e1b5f497 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -133,7 +133,6 @@ "randomizer.overworld.crossed": "Crossed", "randomizer.overworld.crossed.none": "None", - "randomizer.overworld.crossed.allowed": "None (Allowed)", "randomizer.overworld.crossed.polar": "Polar", "randomizer.overworld.crossed.grouped": "Grouped", "randomizer.overworld.crossed.limited": "Limited", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index 9595ea8e..dc0363c6 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -20,7 +20,6 @@ "default": "vanilla", "options": [ "none", - "allowed", "polar", "grouped", "limited", From 8a5a0925228b6035a8fb79b95e466e24dedf1078 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 1 Aug 2022 19:42:54 -0500 Subject: [PATCH 244/293] Adding attribute to world object to indicate whether world is a copy --- BaseClasses.py | 4 +++- Main.py | 1 + Rules.py | 13 ++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e37b8d47..7efa1903 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -62,6 +62,8 @@ class World(object): self.aga_randomness = True self.lock_aga_door_in_escape = False self.save_and_quit_from_boss = True + self.override_bomb_check = False + self.is_copied_world = False self.accessibility = accessibility.copy() self.fix_skullwoods_exit = {} self.fix_palaceofdarkness_exit = {} @@ -1306,7 +1308,7 @@ class CollectionState(object): # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player) or self.has('Bomb Upgrade (+5)', player, 2)) and ((hasattr(self.world,"override_bomb_check") and self.world.override_bomb_check) or self.can_farm_bombs(player)) + return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player) or self.has('Bomb Upgrade (+5)', player, 2)) and (self.world.override_bomb_check or self.can_farm_bombs(player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) diff --git a/Main.py b/Main.py index 6241a830..37e67d6b 100644 --- a/Main.py +++ b/Main.py @@ -557,6 +557,7 @@ def copy_world(world, partial_copy=False): if partial_copy: # undo some of the things that unintentionally affect the original world object world.key_logic = {} + ret.is_copied_world = True return ret diff --git a/Rules.py b/Rules.py index 7e020507..cc9bead3 100644 --- a/Rules.py +++ b/Rules.py @@ -21,12 +21,12 @@ def set_rules(world, player): global_rules(world, player) default_rules(world, player) - ow_rules(world, player) + ow_inverted_rules(world, player) ow_bunny_rules(world, player) if world.mode[player] == 'standard': - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: standard_rules(world, player) elif world.mode[player] == 'open' or world.mode[player] == 'inverted': open_rules(world, player) @@ -805,7 +805,6 @@ def pot_rules(world, player): add_rule(l, lambda state: state.can_hit_crystal(player)) - def default_rules(world, player): set_rule(world.get_entrance('Other World S&Q', player), lambda state: state.has_Mirror(player) and state.has_beaten_aga(player)) @@ -825,7 +824,7 @@ def default_rules(world, player): # Bonk Item Access if world.shuffle_bonk_drops[player]: - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: from Regions import bonk_prize_table for location_name, (_, _, aga_required, _, _, _) in bonk_prize_table.items(): loc = world.get_location(location_name, player) @@ -958,7 +957,7 @@ def default_rules(world, player): swordless_rules(world, player) -def ow_rules(world, player): +def ow_inverted_rules(world, player): if world.is_atgt_swapped(player): set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) else: @@ -1481,7 +1480,7 @@ def no_glitches_rules(world, player): # add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override forbid_bomb_jump_requirements(world, player) - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent underworld rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: add_conditional_lamps(world, player) @@ -1744,7 +1743,7 @@ def standard_rules(world, player): add_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Zelda Delivered', player)) if world.shuffle_bonk_drops[player]: - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: add_rule(world.get_location('Hyrule Castle Tree', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) From ddba1cd8139d6d7f1fefbaa47abdeaa0c0080936 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 1 Aug 2022 20:54:29 -0500 Subject: [PATCH 245/293] Creating a separate copy_world_limited for OWR/ER purposes --- EntranceShuffle.py | 19 +++--- Main.py | 139 +++++++++++++++++++++++++++++++++++++++++--- OverworldShuffle.py | 30 +++++++--- 3 files changed, 160 insertions(+), 28 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index b15a7438..758046c1 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3,6 +3,7 @@ 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 @@ -831,8 +832,6 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must """This works inplace""" random.shuffle(entrances) random.shuffle(caves) - - from DoorShuffle import find_inaccessible_regions used_caves = [] required_entrances = 0 # Number of entrances reserved for used_caves @@ -1274,7 +1273,6 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): dw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) # determine must-exit entrances - from DoorShuffle import find_inaccessible_regions find_inaccessible_regions(world, player) lw_must_exit = list() @@ -1442,13 +1440,12 @@ def place_old_man(world, pool, player, ignore_list=[]): def junk_fill_inaccessible(world, player): - from Main import copy_world - from DoorShuffle import find_inaccessible_regions + from Main import copy_world_limited find_inaccessible_regions(world, player) for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True # remove regions that have a dungeon entrance @@ -1488,7 +1485,6 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe random.shuffle(lw_entrances) random.shuffle(dw_entrances) - from DoorShuffle import find_inaccessible_regions find_inaccessible_regions(world, player) # remove regions that have a dungeon entrance @@ -1611,12 +1607,12 @@ def unbias_dungeons(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 + from Main import copy_world_limited from Items import ItemFactory for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True connect_simple(base_world, 'Links House S&Q', start_region, player) @@ -1719,13 +1715,12 @@ def get_distant_entrances(world, start_entrance, player): def can_reach(world, entrance_name, region_name, player): - from Main import copy_world + from Main import copy_world_limited from Items import ItemFactory - from DoorShuffle import find_inaccessible_regions for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True entrance = world.get_entrance(entrance_name, player) diff --git a/Main.py b/Main.py index 37e67d6b..1649b28b 100644 --- a/Main.py +++ b/Main.py @@ -396,7 +396,7 @@ def main(args, seed=None, fish=None): return world -def copy_world(world, partial_copy=False): +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, @@ -543,10 +543,9 @@ def copy_world(world, partial_copy=False): ret.dungeon_layouts = world.dungeon_layouts ret.key_logic = world.key_logic ret.dungeon_portals = world.dungeon_portals - if not partial_copy: - for player, portals in world.dungeon_portals.items(): - for portal in portals: - connect_portal(portal, ret, player) + for player, portals in world.dungeon_portals.items(): + for portal in portals: + connect_portal(portal, ret, player) ret.sanc_portal = world.sanc_portal from OverworldShuffle import categorize_world_regions @@ -554,10 +553,132 @@ def copy_world(world, partial_copy=False): categorize_world_regions(ret, player) set_rules(ret, player) - if partial_copy: - # undo some of the things that unintentionally affect the original world object - world.key_logic = {} - ret.is_copied_world = True + return ret + + +def copy_world_limited(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.retro, world.custom, world.customitemarray, world.hints) + ret.teams = world.teams + ret.player_names = copy.deepcopy(world.player_names) + ret.remote_items = world.remote_items.copy() + ret.required_medallions = world.required_medallions.copy() + ret.bottle_refills = world.bottle_refills.copy() + ret.swamp_patch_required = world.swamp_patch_required.copy() + ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() + ret.powder_patch_required = world.powder_patch_required.copy() + ret.ganonstower_vanilla = world.ganonstower_vanilla.copy() + ret.treasure_hunt_count = world.treasure_hunt_count.copy() + ret.treasure_hunt_icon = world.treasure_hunt_icon.copy() + ret.sewer_light_cone = world.sewer_light_cone.copy() + ret.light_world_light_cone = world.light_world_light_cone + ret.dark_world_light_cone = world.dark_world_light_cone + ret.seed = world.seed + ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy() + ret.can_access_trock_front = world.can_access_trock_front.copy() + ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy() + ret.can_access_trock_middle = world.can_access_trock_middle.copy() + ret.can_take_damage = world.can_take_damage + ret.difficulty_requirements = world.difficulty_requirements.copy() + ret.fix_fake_world = world.fix_fake_world.copy() + ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms + ret.mapshuffle = world.mapshuffle.copy() + ret.compassshuffle = world.compassshuffle.copy() + ret.keyshuffle = world.keyshuffle.copy() + ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.bombbag = world.bombbag.copy() + ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() + ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() + ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() + ret.crystals_gt_orig = world.crystals_gt_orig.copy() + ret.owKeepSimilar = world.owKeepSimilar.copy() + ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() + ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() + ret.open_pyramid = world.open_pyramid.copy() + ret.boss_shuffle = world.boss_shuffle.copy() + ret.enemy_shuffle = world.enemy_shuffle.copy() + ret.enemy_health = world.enemy_health.copy() + ret.enemy_damage = world.enemy_damage.copy() + ret.beemizer = world.beemizer.copy() + ret.intensity = world.intensity.copy() + ret.experimental = world.experimental.copy() + ret.shopsanity = world.shopsanity.copy() + ret.dropshuffle = world.dropshuffle.copy() + ret.pottery = world.pottery.copy() + ret.potshuffle = world.potshuffle.copy() + ret.mixed_travel = world.mixed_travel.copy() + ret.standardize_palettes = world.standardize_palettes.copy() + ret.owswaps = world.owswaps.copy() + ret.owflutespots = world.owflutespots.copy() + ret.prizes = world.prizes.copy() + ret.restrict_boss_items = world.restrict_boss_items.copy() + + ret.is_copied_world = True + + for player in range(1, world.players + 1): + create_regions(ret, player) + update_world_regions(ret, player) + create_flute_exits(ret, player) + create_dungeon_regions(ret, player) + create_shops(ret, player) + create_rooms(ret, player) + create_dungeons(ret, player) + if world.logic[player] in ('owglitches', 'nologic'): + create_owg_connections(ret, player) + + # # there are region references here they must be migrated to preserve integrity + # # ret.exp_cache = world.exp_cache.copy() + + # copy_dynamic_regions_and_locations(world, ret) + for player in range(1, world.players + 1): + if world.mode[player] == 'standard': + parent = ret.get_region('Menu', player) + target = ret.get_region('Hyrule Castle Secret Entrance', player) + connection = Entrance(player, 'Uncle S&Q', parent) + parent.exits.append(connection) + connection.connect(target) + + # connect copied world + copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations + for region in world.regions: + copied_region = ret.get_region(region.name, region.player) + copied_region.is_light_world = region.is_light_world + copied_region.is_dark_world = region.is_dark_world + copied_region.dungeon = region.dungeon + copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations] + for location in copied_region.locations: + location.parent_region = copied_region + for entrance in region.entrances: + ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + + for item in world.precollected_items: + ret.push_precollected(ItemFactory(item.name, item.player)) + + # copy progress items in state + ret.state.prog_items = world.state.prog_items.copy() + ret.state.stale = {player: True for player in range(1, world.players + 1)} + + ret.owedges = world.owedges.copy() + ret.doors = world.doors.copy() + for door in ret.doors: + entrance = ret.check_for_entrance(door.name, door.player) + if entrance is not None: + entrance.door = door + ret.paired_doors = world.paired_doors.copy() + ret.rooms = world.rooms.copy() + ret.inaccessible_regions = world.inaccessible_regions.copy() + ret.dungeon_layouts = world.dungeon_layouts.copy() + ret.key_logic = world.key_logic.copy() + ret.dungeon_portals = world.dungeon_portals.copy() + ret.sanc_portal = world.sanc_portal.copy() + + from OverworldShuffle import categorize_world_regions + for player in range(1, world.players + 1): + categorize_world_regions(ret, player) + set_rules(ret, player) return ret diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 1e819ae4..140cc346 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -289,6 +289,8 @@ def link_overworld(world, player): for whirlpools in whirlpool_candidates: random.shuffle(whirlpools) while len(whirlpools): + if len(whirlpools) % 2 == 1: + x=0 from_owid, from_whirlpool, from_region = whirlpools.pop() to_owid, to_whirlpool, to_region = whirlpools.pop() connect_simple(world, from_whirlpool, to_region, player) @@ -329,7 +331,7 @@ def link_overworld(world, player): # layout shuffle groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player) - tries = 20 + tries = 100 valid_layout = False connected_edge_cache = connected_edges.copy() while not valid_layout and tries > 0: @@ -426,6 +428,7 @@ def link_overworld(world, player): flute_pool.remove(owid) if ignore_proximity: logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}') + logging.getLogger('').debug(f'Placing flute at: {hex(owid)}') new_spots.append(owid) return True @@ -442,6 +445,7 @@ def link_overworld(world, player): sector_total -= 1 spots_to_place = min(flute_spots - sector_total, max(1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5))) target_spots = len(new_spots) + spots_to_place + logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)') if 'Desert Palace Teleporter Ledge' in sector[1] or 'Misery Mire Teleporter Ledge' in sector[1]: addSpot(0x38, False) # guarantee desert/mire access @@ -878,13 +882,13 @@ def can_reach_smith(world, player): return found def build_sectors(world, player): - from Main import copy_world + from Main import copy_world_limited from OWEdges import OWTileRegions # perform accessibility check on duplicate world for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) regions = list(OWTileRegions.copy().keys()) @@ -928,11 +932,23 @@ def build_sectors(world, player): sectors2.append(explored_regions) sectors[s] = sectors2 + #TODO: Keep largest LW sector for Links House consideration, keep sector containing WDM for Old Man consideration + # sector_entrances = list() + # for sector in sectors: + # entrances = list() + # for s2 in sector: + # for region_name in s2: + # region = world.get_region(region_name, player) + # for exit in region.exits: + # if exit.spot_type == 'Entrance' and exit.name in entrance_pool: + # entrances.append(exit.name) + # sector_entrances.append(entrances) + return sectors def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): - from Main import copy_world from BaseClasses import CollectionState + from Main import copy_world_limited from Items import ItemFactory from Utils import stack_size3a @@ -959,7 +975,7 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F if build_copy_world: for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True else: base_world = world @@ -1015,7 +1031,7 @@ def validate_layout(world, player): entrance_connectors['Bumper Cave Entrance'] = ['West Dark Death Mountain (Bottom)'] entrance_connectors['Mountain Entry Entrance'] = ['Mountain Entry Ledge'] - from Main import copy_world + from Main import copy_world_limited 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 @@ -1048,7 +1064,7 @@ def validate_layout(world, player): for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) explored_regions = list() if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]: From 64c65f680f8365d19d9f9955abd6fdea63179a0f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 1 Aug 2022 23:48:54 -0500 Subject: [PATCH 246/293] Creating a separate copy_world_limited for OWR/ER purposes --- Main.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Main.py b/Main.py index 1649b28b..e2a3efc7 100644 --- a/Main.py +++ b/Main.py @@ -629,10 +629,6 @@ def copy_world_limited(world): if world.logic[player] in ('owglitches', 'nologic'): create_owg_connections(ret, player) - # # there are region references here they must be migrated to preserve integrity - # # ret.exp_cache = world.exp_cache.copy() - - # copy_dynamic_regions_and_locations(world, ret) for player in range(1, world.players + 1): if world.mode[player] == 'standard': parent = ret.get_region('Menu', player) @@ -657,23 +653,13 @@ def copy_world_limited(world): for item in world.precollected_items: ret.push_precollected(ItemFactory(item.name, item.player)) - # copy progress items in state - ret.state.prog_items = world.state.prog_items.copy() - ret.state.stale = {player: True for player in range(1, world.players + 1)} - ret.owedges = world.owedges.copy() ret.doors = world.doors.copy() for door in ret.doors: entrance = ret.check_for_entrance(door.name, door.player) if entrance is not None: entrance.door = door - ret.paired_doors = world.paired_doors.copy() - ret.rooms = world.rooms.copy() - ret.inaccessible_regions = world.inaccessible_regions.copy() - ret.dungeon_layouts = world.dungeon_layouts.copy() ret.key_logic = world.key_logic.copy() - ret.dungeon_portals = world.dungeon_portals.copy() - ret.sanc_portal = world.sanc_portal.copy() from OverworldShuffle import categorize_world_regions for player in range(1, world.players + 1): From e847b213609fbd1fe9ff1ae56ea94b455d82e28c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 2 Aug 2022 00:14:08 -0500 Subject: [PATCH 247/293] Creating a separate copy_world_limited for OWR/ER purposes --- Main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Main.py b/Main.py index e2a3efc7..4e05fb83 100644 --- a/Main.py +++ b/Main.py @@ -659,6 +659,7 @@ def copy_world_limited(world): entrance = ret.check_for_entrance(door.name, door.player) if entrance is not None: entrance.door = door + door.entrance = entrance ret.key_logic = world.key_logic.copy() from OverworldShuffle import categorize_world_regions From bc4b16d910b90b68cfdd1ebbb2ab027418d28f8c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 2 Aug 2022 10:34:15 -0500 Subject: [PATCH 248/293] Creating a separate copy_world_limited for OWR/ER purposes --- Main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 4e05fb83..44b88ffd 100644 --- a/Main.py +++ b/Main.py @@ -621,13 +621,14 @@ def copy_world_limited(world): for player in range(1, world.players + 1): create_regions(ret, player) update_world_regions(ret, player) + if world.logic[player] in ('owglitches', 'nologic'): + create_owg_connections(ret, player) create_flute_exits(ret, player) create_dungeon_regions(ret, player) create_shops(ret, player) + create_doors(ret, player) create_rooms(ret, player) create_dungeons(ret, player) - if world.logic[player] in ('owglitches', 'nologic'): - create_owg_connections(ret, player) for player in range(1, world.players + 1): if world.mode[player] == 'standard': @@ -644,7 +645,7 @@ def copy_world_limited(world): copied_region.is_light_world = region.is_light_world copied_region.is_dark_world = region.is_dark_world copied_region.dungeon = region.dungeon - copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations] + copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations if (location.name, location.player) in copied_locations] for location in copied_region.locations: location.parent_region = copied_region for entrance in region.entrances: @@ -654,12 +655,10 @@ def copy_world_limited(world): ret.push_precollected(ItemFactory(item.name, item.player)) ret.owedges = world.owedges.copy() - ret.doors = world.doors.copy() for door in ret.doors: entrance = ret.check_for_entrance(door.name, door.player) if entrance is not None: entrance.door = door - door.entrance = entrance ret.key_logic = world.key_logic.copy() from OverworldShuffle import categorize_world_regions From 74912e3bedaba40ac0e272fb36bb7ef9aac5ea64 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 2 Aug 2022 10:56:09 -0500 Subject: [PATCH 249/293] Fixed issue with flute shuffle placing spots not in pool --- OverworldShuffle.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 140cc346..4ae0ee58 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -425,11 +425,15 @@ def link_overworld(world, player): if not ignore_proximity and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions): return False ignored_regions.update(new_ignored) - flute_pool.remove(owid) - if ignore_proximity: - logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}') - logging.getLogger('').debug(f'Placing flute at: {hex(owid)}') - new_spots.append(owid) + if owid in flute_pool: + flute_pool.remove(owid) + if ignore_proximity: + logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}') + logging.getLogger('').debug(f'Placing flute at: {hex(owid)}') + new_spots.append(owid) + else: + # TODO: Inspect later, seems to happen only with 'random' flute shuffle + logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}') return True # determine sectors (isolated groups of regions) to place flute spots From 85fe7d1d04f5d9bd674b999e955b66a66d60892c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 3 Aug 2022 17:52:22 -0500 Subject: [PATCH 250/293] Fixed bomb/rupee farming to not include caves if cave pots are shuffled --- BaseClasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7efa1903..dc943c2b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1106,7 +1106,7 @@ class CollectionState(object): region = self.world.get_region(regionname, player) return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) - for region in rupee_farms: + for region in rupee_farms if self.world.pottery[player] in ['none', 'keys', 'dungeon'] else ['Archery Game']: if can_reach_non_bunny(region): return True @@ -1188,7 +1188,7 @@ class CollectionState(object): return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) # bomb pickups - for region in bush_bombs + bomb_caves: + for region in bush_bombs + (bomb_caves if self.world.pottery[player] in ['none', 'keys', 'dungeon'] else []): if can_reach_non_bunny(region): return True From 32714b4c6a35f4bdd4779aed8619f43423b6c021 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 5 Aug 2022 16:48:34 -0600 Subject: [PATCH 251/293] Fix for colorized pots in "dark desert hint" aka mire storyteller --- Main.py | 2 +- PotShuffle.py | 13 +++++++------ RELEASENOTES.md | 4 +++- Rom.py | 25 +++++++++++++------------ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Main.py b/Main.py index 7aba1276..3787e81b 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.1.0-u' +__version__ = '1.0.1.1-u' from source.classes.BabelFish import BabelFish diff --git a/PotShuffle.py b/PotShuffle.py index a1c96783..7806f495 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -787,12 +787,13 @@ vanilla_pots = { Pot(230, 27, PotItem.Bomb, 'Light World Bomb Hut', obj=RoomObject(0x03EF5E, [0xCF, 0xDF, 0xFA]))], 0x108: [Pot(166, 19, PotItem.Chicken, 'Chicken House', obj=RoomObject(0x03EFA9, [0x4F, 0x9F, 0xFA]))], 0x10C: [Pot(88, 14, PotItem.Heart, 'Hookshot Fairy', obj=RoomObject(0x03F329, [0xB3, 0x73, 0xFA]))], - 0x114: [Pot(92, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A0, [0xBB, 0x23, 0xFA])), - Pot(96, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A3, [0xC3, 0x23, 0xFA])), - Pot(92, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A6, [0xBB, 0x2B, 0xFA])), - Pot(96, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A9, [0xC3, 0x2B, 0xFA])), - Pot(92, 10, PotItem.FiveArrows, 'Dark Desert Hint', obj=RoomObject(0x03F7AC, [0xBB, 0x53, 0xFA])), - Pot(96, 10, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7AF, [0xC3, 0x53, 0xFA]))], + # note: these addresses got moved thanks to waterfall fairy edit + 0x114: [Pot(92, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F79A, [0xBB, 0x23, 0xFA])), + Pot(96, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F79D, [0xC3, 0x23, 0xFA])), + Pot(92, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A0, [0xBB, 0x2B, 0xFA])), + Pot(96, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A3, [0xC3, 0x2B, 0xFA])), + Pot(92, 10, PotItem.FiveArrows, 'Dark Desert Hint', obj=RoomObject(0x03F7A6, [0xBB, 0x53, 0xFA])), + Pot(96, 10, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A9, [0xC3, 0x53, 0xFA]))], 0x117: [Pot(138, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCB2, [0x17, 0x1F, 0xFA])), # 0x38A -> 38A Pot(142, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCB8, [0x1F, 0x1F, 0xFA])), Pot(166, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCC1, [0x4F, 0x1F, 0xFA])), diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 65293af1..ce3e948c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -183,6 +183,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable +* 1.0.1.1 + * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be * 1.0.1.0 * Large features * New pottery modes - see notes above @@ -235,7 +237,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count * Fixed usestartinventory flag (can be use on a per player basis) * Sprite selector fix for systems with SSL issues - * Fix for Standard ER where locations in rain state could be in logic + * Fix for Standard ER where locations in rain state could be in logic * 1.0.0.3 * overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown) * Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix) diff --git a/Rom.py b/Rom.py index 7861a5cd..3a4661da 100644 --- a/Rom.py +++ b/Rom.py @@ -663,18 +663,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.mapshuffle[player]: rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle - if world.pottery[player] not in ['none']: - rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2A8000)) - # make hammer pegs use different tiles - Room0127.write_to_rom(snes_to_pc(0x2A8000), rom) - - if world.pot_contents[player]: - colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery'] - and (world.colorizepots[player] - or world.pottery[player] in ['reduced', 'clustered'])) - if world.pot_contents[player].size() > 0x2800: - raise Exception('Pot table is too big for current area') - world.pot_contents[player].write_pot_data_to_rom(rom, colorize_pots) # fix for swamp drains if necessary swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player) if not swamp1location.pot.indicator: @@ -1545,6 +1533,19 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if room.player == player and room.modified: rom.write_bytes(room.address(), room.rom_data()) + if world.pottery[player] not in ['none']: + rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2A8000)) + # make hammer pegs use different tiles + Room0127.write_to_rom(snes_to_pc(0x2A8000), rom) + + if world.pot_contents[player]: + colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery'] + and (world.colorizepots[player] + or world.pottery[player] in ['reduced', 'clustered'])) + if world.pot_contents[player].size() > 0x2800: + raise Exception('Pot table is too big for current area') + world.pot_contents[player].write_pot_data_to_rom(rom, colorize_pots) + write_strings(rom, world, player, team) # write initial sram From 2ed0a806092d19c8713abbdb70bf73ac75f52e13 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 11 Aug 2022 15:25:09 -0600 Subject: [PATCH 252/293] Fix for pot items to not reload with the supertile Key distribution change Unique boss shuffle make gt bosses also unique Removed text color in hints due to bug --- Bosses.py | 5 ++- Fill.py | 85 ++++++++++++++++++++++-------------------- RELEASENOTES.md | 7 +++- Rom.py | 10 ++--- data/base2current.bps | Bin 93156 -> 93186 bytes 5 files changed, 56 insertions(+), 51 deletions(-) diff --git a/Bosses.py b/Bosses.py index 53393d5f..206b797c 100644 --- a/Bosses.py +++ b/Bosses.py @@ -202,12 +202,15 @@ def place_bosses(world, player): place_boss(boss, level, loc, loc_text, world, player) elif world.boss_shuffle[player] == 'unique': bosses = list(placeable_bosses) + gt_bosses = [] for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') try: if level: - boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)]) + boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level) + and b not in gt_bosses]) + gt_bosses.append(boss) else: boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) bosses.remove(boss) diff --git a/Fill.py b/Fill.py index 74274543..bb6e70cf 100644 --- a/Fill.py +++ b/Fill.py @@ -3,6 +3,7 @@ import collections import itertools import logging import math +from contextlib import suppress from BaseClasses import CollectionState, FillError, LocationType from Items import ItemFactory @@ -35,17 +36,6 @@ def dungeon_tracking(world): def fill_dungeons_restrictive(world, shuffled_locations): dungeon_tracking(world) - all_state_base = world.get_all_state() - - # for player in range(1, world.players + 1): - # pinball_room = world.get_location('Skull Woods - Pinball Room', player) - # if world.retro[player]: - # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - # else: - # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - # pinball_room.event = True - # pinball_room.locked = True - # shuffled_locations.remove(pinball_room) # with shuffled dungeon items they are distributed as part of the normal item pool for item in world.get_items(): @@ -55,17 +45,28 @@ def fill_dungeons_restrictive(world, shuffled_locations): item.priority = True dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] + bigs, smalls, others = [], [], [] + for i in dungeon_items: + (bigs if i.bigkey else smalls if i.smallkey else others).append(i) - # sort in the order Big Key, Small Key, Other before placing dungeon items - sort_order = {"BigKey": 3, "SmallKey": 2} - dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) + def fill(base_state, items, key_pool): + fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) - fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, - keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, - single_player_placement=True) + all_state_base = world.get_all_state() + big_state_base = all_state_base.copy() + for x in smalls + others: + big_state_base.collect(x, True) + fill(big_state_base, bigs, smalls) + random.shuffle(shuffled_locations) + small_state_base = all_state_base.copy() + for x in others: + small_state_base.collect(x, True) + fill(small_state_base, smalls, smalls) + random.shuffle(shuffled_locations) + fill(all_state_base, others, None) -def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, +def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False, vanilla=False): def sweep_from_pool(): new_state = base_state.copy() @@ -101,8 +102,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No item_locations = filter_locations(item_to_place, locations, world, vanilla) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, - single_player_placement, perform_access_check, itempool, - keys_in_itempool, world) + single_player_placement, perform_access_check, key_pool, world) if spot_to_fill: break if spot_to_fill is None: @@ -111,7 +111,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No continue spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, base_state, itempool, perform_access_check, item_locations, - keys_in_itempool, single_player_placement) + key_pool, single_player_placement) if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) @@ -123,6 +123,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) + # todo: remove key item from key_pool + if item_to_place.smallkey: + with suppress(ValueError): + key_pool.remove(item_to_place) track_outside_keys(item_to_place, spot_to_fill, world) track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) @@ -132,7 +136,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, - itempool, keys_in_itempool, world): + key_pool, world): if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place test_state = max_exp_state.copy() @@ -141,8 +145,7 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl test_state = max_exp_state if not single_player_placement or location.player == item_to_place.player: if location.can_fill(test_state, item_to_place, perform_access_check): - test_pool = itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool - if valid_key_placement(item_to_place, location, test_pool, world): + if valid_key_placement(item_to_place, location, key_pool, world): if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): return location if item_to_place.smallkey or item_to_place.bigkey: @@ -150,7 +153,7 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl return None -def valid_key_placement(item, location, itempool, world): +def valid_key_placement(item, location, key_pool, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player @@ -161,7 +164,7 @@ def valid_key_placement(item, location, itempool, world): if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name): return True key_logic = world.key_logic[item.player][dungeon.name] - unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) + 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) @@ -216,16 +219,16 @@ def is_dungeon_item(item, world): def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, - keys_in_itempool=None, single_player_placement=False): + key_pool=None, single_player_placement=False): logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery') if world.algorithm in ['balanced', 'equitable']: - return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, + 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'] return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, - keys_in_itempool, single_player_placement) + key_pool, single_player_placement) else: i, config = 0, world.item_pool_config tried = set(attempted) @@ -235,7 +238,7 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp other_locs = [x for x in locations if x.name in fallback_locations] for location in other_locs: spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, - perform_access_check, itempool, keys_in_itempool, world) + perform_access_check, key_pool, world) if spot_to_fill: return spot_to_fill i += 1 @@ -244,14 +247,14 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp other_locations = vanilla_fallback(item_to_place, locations, world) for location in other_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, - perform_access_check, itempool, keys_in_itempool, world) + perform_access_check, key_pool, world) if spot_to_fill: return spot_to_fill tried.update(other_locations) other_locations = [x for x in locations if x not in tried] for location in other_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, - perform_access_check, itempool, keys_in_itempool, world) + perform_access_check, key_pool, world) if spot_to_fill: return spot_to_fill return None @@ -259,14 +262,14 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp other_locations = [x for x in locations if x not in attempted] for location in other_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, - perform_access_check, itempool, keys_in_itempool, world) + perform_access_check, key_pool, world) if spot_to_fill: return spot_to_fill return None def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, - keys_in_itempool=None, single_player_placement=False): + key_pool=None, single_player_placement=False): def location_preference(loc): if not loc.item.advancement: return 1 @@ -284,21 +287,21 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, - keys_in_itempool, single_player_placement) + key_pool, single_player_placement) def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, - keys_in_itempool=None, single_player_placement=False): + key_pool=None, single_player_placement=False): for location in swap_locations: old_item = location.item new_pool = list(itempool) + [old_item] new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, - keys_in_itempool, single_player_placement) + key_pool, single_player_placement) if new_spot: restore_item = new_spot.item new_spot.item = item_to_place swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, - keys_in_itempool, single_player_placement) + key_pool, single_player_placement) if swap_spot: logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') world.push_item(swap_spot, old_item, False) @@ -414,13 +417,13 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # todo: crossed progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) - keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)} + key_pool = [x for x in progitempool if x.smallkey] # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) if world.algorithm == 'vanilla_fill': - fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool, vanilla=True) - fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + fill_restrictive(world, world.state, fill_locations, progitempool, key_pool, vanilla=True) + fill_restrictive(world, world.state, fill_locations, progitempool, key_pool) random.shuffle(fill_locations) if world.algorithm == 'balanced': fast_fill(world, prioitempool, fill_locations) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce3e948c..56ecca32 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,11 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable * 1.0.1.1 - * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be + * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be + * Certain pot items no longer reload when reloading the supertile (matches original pot behavior better) + * Changed the key distribution that made small keys placement more random when keys are in their own dungeon + * Unique boss shuffle no longer allows repeat bosses in GT (e.g. only one Trinexx in GT, so exactly 3 bosses are repeated in the seed. This is a difference process than full which does affect the probability distribution.) + * Removed text color in hints due to vanilla bug * 1.0.1.0 * Large features * New pottery modes - see notes above @@ -208,7 +212,6 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work. * Updated tourney winners (included Doors Async League winners) * Some textual changes for hints (capitalization standardization) - * Item will be highlighted in red if experimental is on. This will likely be removed. * Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill. * Expanded Mystery logic options (e.g. owglitches) * Updated indicators on keysanity menu for overworld map option diff --git a/Rom.py b/Rom.py index 3a4661da..c3312b9a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9008f4335101689f01184e58295fdbc5' +RANDOMIZERBASEHASH = '0f96237c73cccaf7a250343fe3e8c887' class JsonRom(object): @@ -1979,8 +1979,6 @@ def write_strings(rom, world, player, team): else: if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon: hint = dest.dungeon.name - elif isinstance(dest, Item) and world.experimental[player]: - hint = f'{{C:RED}}{dest.hint_text}{{C:WHITE}}' if dest.hint_text else 'something' else: hint = dest.hint_text if dest.hint_text else "something" if dest.player != player: @@ -2149,8 +2147,7 @@ def write_strings(rom, world, player, team): if this_location: item_name = this_location[0].item.hint_text item_name = item_name[0].upper() + item_name[1:] - item_format = f'{{C:RED}}{item_name}{{C:WHITE}}' if world.experimental[player] else item_name - this_hint = f'{item_format} can be found {hint_text(this_location[0])}.' + this_hint = f'{item_name} can be found {hint_text(this_location[0])}.' tt[hint_locations.pop(0)] = this_hint hint_count -= 1 @@ -2204,8 +2201,7 @@ def write_strings(rom, world, player, team): elif hint_type == 'path': if item_count == 1: the_item = text_for_item(next(iter(choice_set)), world, player, team) - item_format = f'{{C:RED}}{the_item}{{C:WHITE}}' if world.experimental[player] else the_item - hint_candidates.append((hint_type, f'{name} conceals only {item_format}')) + hint_candidates.append((hint_type, f'{name} conceals only {the_item}')) else: hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items')) district_hints = min(len(hint_candidates), len(hint_locations)) diff --git a/data/base2current.bps b/data/base2current.bps index 464ceaf6105a2637981d2d107a868a08a143c8e4..55525ee50a0d8f30527d68ac4de47956a9acb906 100644 GIT binary patch delta 476 zcmV<20VDq8*9C&u1(1pYAF`2*#tV%w0t*F*sWpwta+6*GE&<)MjRB1X22-)9#EFEH zAP0W|xs#v=kU9;osOsvNl^?LEfC1_}h?OE4v8b>ImnkxVr3oTnu&5d$keDZ+l_F@b zsOstzfsG<3)SyrefRQ5FlWYh&6`iiA>U4mK1InOWfQc`$sA#jE03Nfu2tok}!m+68 zxQ!yyvjqxX0th>45vQ=0)Vp=Or+2X?XIYW3Xbs6Al8?{9062b34x6u z2aX~K?j{GQsR#hTHUQ8CsTKf$BEUod&;_X%0Du_4C;-p}sTcr&4!|)0&;_X(0DuII zoC|A6iVB4QsUHC40GA_xlS6%j#*m?CjhqW>Xq=o2pFVzrj@Oq`9RX?qZkN3s0b&7v zmmnSiDh!(p9FC3py>JJcD|we!9sws0N`8ZZ8LNVguObJ2n`MWWjUEAB0x%hu?H&Or z0qK_(9|0c`MzHi0fLuD#25Ta6vZ%*JR+mN}0Wtyqmw6unH39LLuO9(A0r8jW9|2+v z^sS3atGQ6It6(5%mslVHrU7V|{~!TE3mvn760?M{!XT;5mrNl6Hv$+fmxUn#Is!{$ Sx4|I+8%hYs1;u%{PR&`PFuCsl delta 471 zcmV;|0Vw{0*ahU*1(1pY?6HxI#tg+U0t*EI0RY>iBwLeR0WJZmvy1_a1qL9osKkj; zlOG3v0fv*G2ar74uBhtjnlHVT?60VR0qQ)6l_KV_sIUi@DKdej2_h}9s2U=Wm?xl> zA~mn5>gp7MjUp%1pim8fks_y)X$U$MYObj2bbyHi%Aj0;iTtprXtSOmT(h_cLIDVm zv8d{}jUu440t#LN2nepI9Dz%c;O1F0DR zjhqXgk&dI6aUB6_0SK4e9RXqi8J9mE0V)lbA{ma60Q$Xf2b({3mvtTiCl6$OgMk^V zf{m{t2Y#D>hL@`z0bT-17?%ql0VpxHv8ZTkB7y%NNWFj#$t2JnljMO4lY-R*sU(06 zXiM}Hu=EswTsqPQYa&0gsK;Mbmt-FSG6Bq&mmdK&0lt^b9|1Z6zLy9f0b&fnt&2;m zxgD{qU?4Y_b|3+!0X3H$Apt@Q>au_mvxKq2AgP*{Y9RqP0x~R@qagu00vBbs-ys1T NN(i8Xyg-eA7zmvww~7D& From 3aa97ee8bcc0496fc001adf786f7e43af800a6d9 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 12 Aug 2022 01:53:13 -0500 Subject: [PATCH 253/293] Fixed early rupee money balancing to make item free if <20 rupees available --- Fill.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Fill.py b/Fill.py index daa6f6bd..6bf95e64 100644 --- a/Fill.py +++ b/Fill.py @@ -750,12 +750,14 @@ def balance_multiworld_progression(world): raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') -def check_shop_swap(l): +def check_shop_swap(l, make_item_free=False): if l.parent_region.name in shop_to_location_table: if l.name in shop_to_location_table[l.parent_region.name]: idx = shop_to_location_table[l.parent_region.name].index(l.name) inv_slot = l.parent_region.shop.inventory[idx] inv_slot['item'] = l.item.name + if make_item_free: + inv_slot['price'] = 0 elif l.parent_region in retro_shops: idx = retro_shops[l.parent_region.name].index(l.name) inv_slot = l.parent_region.shop.inventory[idx] @@ -921,12 +923,13 @@ def balance_money_progression(world): if len(increase_targets) == 0: raise Exception('No early sphere swaps for rupees - money grind would be required - bailing for now') best_target = min(increase_targets, key=lambda t: rupee_chart[t.item.name] if t.item.name in rupee_chart else 0) - old_value = rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0 + make_item_free = wallet[target_player] < 20 + old_value = 0 if make_item_free else (rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0) if best_swap is None: logger.debug(f'Upgrading {best_target.item.name} @ {best_target.name} for 300 Rupees') best_target.item = ItemFactory('Rupees (300)', best_target.item.player) best_target.item.location = best_target - check_shop_swap(best_target.item.location) + check_shop_swap(best_target.item.location, make_item_free) else: old_item = best_target.item logger.debug(f'Swapping {best_target.item.name} @ {best_target.name} for {best_swap.item.name} @ {best_swap.name}') @@ -934,7 +937,7 @@ def balance_money_progression(world): best_target.item.location = best_target best_swap.item = old_item best_swap.item.location = best_swap - check_shop_swap(best_target.item.location) + check_shop_swap(best_target.item.location, make_item_free) check_shop_swap(best_swap.item.location) increase = best_value - old_value difference -= increase From e5cff2e773881a41bacfd45ccaf497e4812056dd Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 12 Aug 2022 01:54:20 -0500 Subject: [PATCH 254/293] Fixed so Lean ER + Inverted Dark Chapel start is guaranteed to be in DW --- OverworldShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 4ae0ee58..304eba95 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -689,7 +689,7 @@ def define_tile_groups(world, player, do_grouped): # sanctuary/chapel should not be swapped if S+Q guaranteed to output on that screen if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ - or (world.shuffle[player] == 'lite' and world.mode[player] == 'inverted')): + or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')): return False return True From 6455cad3610b7764fa0c793c620c5b4331e2bb7f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 17 Aug 2022 14:09:02 -0500 Subject: [PATCH 255/293] Fixed Mystery to detect Windows Paths over URLs --- Mystery.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Mystery.py b/Mystery.py index e1be0eb7..d313ac4e 100644 --- a/Mystery.py +++ b/Mystery.py @@ -4,6 +4,8 @@ import RaceRandom as random import urllib.request import urllib.parse import yaml +import os +from pathlib import Path from DungeonRandomizer import parse_cli from Main import main as DRMain @@ -107,10 +109,11 @@ def main(): def get_weights(path): try: - if urllib.parse.urlparse(path).scheme: + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) - with open(path, 'r', encoding='utf-8') as f: - return yaml.load(f, Loader=yaml.SafeLoader) except Exception as e: raise Exception(f'Failed to read weights file: {e}') From f2d7cdca1b8863dd18463053f8df1f9542b265d1 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 17 Aug 2022 14:38:19 -0500 Subject: [PATCH 256/293] Changed Standard+Inverted to include Sanc screen in fixed Std screens --- OverworldShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 304eba95..289bcf20 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -615,7 +615,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): # tile shuffle happens here removed = list() for group in groups: - # if 0x1b in group[0] or (0x1a in group[0] and world.owCrossed[player] == 'none'): # TODO: Standard + Inverted + #if 0x1b in group[0] or 0x13 in group[0] or (0x1a in group[0] and world.owCrossed[player] == 'none'): # TODO: Standard + Inverted if random.randint(0, 1): removed.append(group) From 76c1fef4b67730328e94efbc7478128a1e2d0e8d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 22 Aug 2022 22:01:48 -0500 Subject: [PATCH 257/293] Absolutely no change whatsoever so help me God --- Rom.py | 2 +- asm/owrando.asm | 1 + data/base2current.bps | Bin 104377 -> 104379 bytes 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index d8fcd686..f41e59e8 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '94a8df9d13105abcd655c7d59294ad20' +RANDOMIZERBASEHASH = 'b1ded43b30364c3e5d9a94afa2f46599' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 04ffd047..68bdbb43 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -502,6 +502,7 @@ OWBonkDrops: .return PLA : PLA : PLB : RTL } +nop org $aa9000 OWDetectEdgeTransition: diff --git a/data/base2current.bps b/data/base2current.bps index af312a912f5341731add985f82cf58223b65bc3a..70c96830f3c9c8da43808270d9beebfb8fe81295 100644 GIT binary patch delta 85 zcmV-b0IL7FuLirX2C!iP1bU{0S%YN(w`BnVv^g%Nk26qzt7~Xzt81GP7^O#_Oevbt rocso_0)VOy1AwaV2q5i{t@;Kq2bK>HkGDlT0c>0d%^rTJJUp)B;Q1p7 delta 66 zcmV-I0KNaauLilV2C!iP1Z1b1Sc7E&w`BnVv^f%;k26qzt7~XzrV2ixL!UKmiI*)q Y0T%-;PO-N~I{|E52-yJo6j4r0&{7r{n*aa+ From 21b951433edf64a09a65185eee2ea4b275d5412a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 22 Aug 2022 22:04:29 -0500 Subject: [PATCH 258/293] Undoing absolutely no change whatsoever so help me God --- Rom.py | 2 +- asm/owrando.asm | 1 - data/base2current.bps | Bin 104379 -> 104377 bytes 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index f41e59e8..d8fcd686 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'b1ded43b30364c3e5d9a94afa2f46599' +RANDOMIZERBASEHASH = '94a8df9d13105abcd655c7d59294ad20' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 68bdbb43..04ffd047 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -502,7 +502,6 @@ OWBonkDrops: .return PLA : PLA : PLB : RTL } -nop org $aa9000 OWDetectEdgeTransition: diff --git a/data/base2current.bps b/data/base2current.bps index 70c96830f3c9c8da43808270d9beebfb8fe81295..af312a912f5341731add985f82cf58223b65bc3a 100644 GIT binary patch delta 66 zcmV-I0KNaauLilV2C!iP1Z1b1Sc7E&w`BnVv^f%;k26qzt7~XzrV2ixL!UKmiI*)q Y0T%-;PO-N~I{|E52-yJo6j4r0&{7r{n*aa+ delta 85 zcmV-b0IL7FuLirX2C!iP1bU{0S%YN(w`BnVv^g%Nk26qzt7~Xzt81GP7^O#_Oevbt rocso_0)VOy1AwaV2q5i{t@;Kq2bK>HkGDlT0c>0d%^rTJJUp)B;Q1p7 From c9823f28af9527cf627ef5cc7e369c1511cfd127 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 22 Aug 2022 23:01:38 -0500 Subject: [PATCH 259/293] Moving Aerinon's room mod somewhere else --- Rom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index d8fcd686..7b437caa 100644 --- a/Rom.py +++ b/Rom.py @@ -1695,9 +1695,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(room.address(), room.rom_data()) if world.pottery[player] not in ['none']: - rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2A8000)) + rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x299000)) # make hammer pegs use different tiles - Room0127.write_to_rom(snes_to_pc(0x2A8000), rom) + Room0127.write_to_rom(snes_to_pc(0x299000), rom) if world.pot_contents[player]: colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery'] From 270d10b6980b69ce3060945d46a3559cd56e7450 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 23 Aug 2022 16:52:43 -0500 Subject: [PATCH 260/293] Possible fix for SK placement errors --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index fa6db391..8dfa69cc 100644 --- a/Fill.py +++ b/Fill.py @@ -61,7 +61,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): small_state_base = all_state_base.copy() for x in others: small_state_base.collect(x, True) - fill(small_state_base, smalls, smalls) + fill(small_state_base, smalls, list(smalls)) random.shuffle(shuffled_locations) fill(all_state_base, others, None) From 0d5ea19ecda2a98809b53b44f8378db70424f608 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 23 Aug 2022 15:52:59 -0600 Subject: [PATCH 261/293] Fix for small key fill --- Fill.py | 3 +-- ItemList.py | 24 ------------------------ 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/Fill.py b/Fill.py index bb6e70cf..b2dc3400 100644 --- a/Fill.py +++ b/Fill.py @@ -61,7 +61,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): small_state_base = all_state_base.copy() for x in others: small_state_base.collect(x, True) - fill(small_state_base, smalls, smalls) + fill(small_state_base, smalls, list(smalls)) random.shuffle(shuffled_locations) fill(all_state_base, others, None) @@ -123,7 +123,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) - # todo: remove key item from key_pool if item_to_place.smallkey: with suppress(ValueError): key_pool.remove(item_to_place) diff --git a/ItemList.py b/ItemList.py index dacbdf14..9dd69157 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1099,27 +1099,3 @@ def test(): if __name__ == '__main__': test() - - -def fill_specific_items(world): - keypool = [item for item in world.itempool if item.smallkey] - cage = world.get_location('Tower of Hera - Basement Cage', 1) - c_dungeon = cage.parent_region.dungeon - key_item = next(x for x in keypool if c_dungeon.name in x.name or (c_dungeon.name == 'Hyrule Castle' and 'Escape' in x.name)) - world.itempool.remove(key_item) - all_state = world.get_all_state(True) - fill_restrictive(world, all_state, [cage], [key_item]) - - location = world.get_location('Tower of Hera - Map Chest', 1) - key_item = next(x for x in world.itempool if 'Byrna' in x.name) - world.itempool.remove(key_item) - fast_fill(world, [key_item], [location]) - - - # somaria = next(item for item in world.itempool if item.name == 'Cane of Somaria') - # shooter = world.get_location('Palace of Darkness - Shooter Room', 1) - # world.itempool.remove(somaria) - # all_state = world.get_all_state(True) - # fill_restrictive(world, all_state, [shooter], [somaria]) - - From 9c612b6c00511226374c1a9be4532892c4177ddd Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 23 Aug 2022 16:43:27 -0600 Subject: [PATCH 262/293] Minor edit to when pyramid hole auto pre-opens --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index c3312b9a..58a144ad 100644 --- a/Rom.py +++ b/Rom.py @@ -1263,7 +1263,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - if world.open_pyramid[player] or world.goal[player] == 'trinity': + if world.open_pyramid[player] or (world.goal[player] in ['trinity', 'crystals'] and world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']): rom.initial_sram.pre_open_pyramid_hole() if world.crystals_needed_for_gt[player] == 0: rom.initial_sram.pre_open_ganons_tower() From a7e9d6d43f9ed9f1266d6a6293cf3be71bad703d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 23 Aug 2022 18:19:17 -0500 Subject: [PATCH 263/293] Creating a separate copy_world_limited for OWR/ER purposes --- EntranceShuffle.py | 2 +- Main.py | 18 ++++++++++++------ Rules.py | 15 ++++++++------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 758046c1..d6c207f2 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -32,7 +32,7 @@ def link_entrances(world, player): Cave_Three_Exits = Cave_Three_Exits_Base.copy() from OverworldShuffle import build_sectors - if not world.owsectors[player]: + if not world.owsectors[player] and world.shuffle[player] != 'vanilla': world.owsectors[player] = build_sectors(world, player) # modifications to lists diff --git a/Main.py b/Main.py index 60bdb40f..0ebf52db 100644 --- a/Main.py +++ b/Main.py @@ -625,8 +625,8 @@ def copy_world_limited(world): create_owg_connections(ret, player) create_flute_exits(ret, player) create_dungeon_regions(ret, player) + create_owedges(ret, player) create_shops(ret, player) - create_doors(ret, player) create_rooms(ret, player) create_dungeons(ret, player) @@ -654,11 +654,17 @@ def copy_world_limited(world): for item in world.precollected_items: ret.push_precollected(ItemFactory(item.name, item.player)) - ret.owedges = world.owedges.copy() - for door in ret.doors: - entrance = ret.check_for_entrance(door.name, door.player) - if entrance is not None: - entrance.door = door + for edge in world.owedges: + copiededge = ret.check_for_owedge(edge.name, edge.player) + if copiededge is not None: + copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) + # for door in world.doors: + # entrance = ret.check_for_entrance(door.name, door.player) + # if entrance is not None: + # destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) + # entrance.door = destdoor + # if destdoor is not None: + # destdoor.entrance = entrance ret.key_logic = world.key_logic.copy() from OverworldShuffle import categorize_world_regions diff --git a/Rules.py b/Rules.py index cc9bead3..260b2893 100644 --- a/Rules.py +++ b/Rules.py @@ -341,7 +341,7 @@ def global_rules(world, player): set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Hookshot Ledge Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player)) - if not world.get_door('Ice Switch Room SE', player).entranceFlag: + if not world.is_copied_world and not world.get_door('Ice Switch Room SE', player).entranceFlag: set_rule(world.get_entrance('Ice Switch Room SE', 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)) @@ -398,7 +398,7 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('GT Conveyor Cross WN', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hookshot', player)) - if not world.get_door('GT Speed Torch SE', player).entranceFlag: + if not world.is_copied_world and not world.get_door('GT Speed Torch SE', player).entranceFlag: set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player)) @@ -421,7 +421,7 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Gauntlet 2 EN', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 2 SW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 3 NW', player), lambda state: state.can_kill_most_things(player)) - if not world.get_door('GT Gauntlet 3 SW', player).entranceFlag: + if not world.is_copied_world and not world.get_door('GT Gauntlet 3 SW', player).entranceFlag: set_rule(world.get_entrance('GT Gauntlet 3 SW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 4 NW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 4 SW', player), lambda state: state.can_kill_most_things(player)) @@ -441,10 +441,11 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) # crystal switch rules - if world.get_door('Thieves Attic ES', player).crystal == CrystalBarrier.Blue: - set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) - else: - set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) + if not world.is_copied_world: + if world.get_door('Thieves Attic ES', player).crystal == CrystalBarrier.Blue: + set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) + else: + set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) set_rule(world.get_entrance('Thieves Attic Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) set_rule(world.get_entrance('Thieves Attic Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) From 34c9972c74980d8625e533226934d774c9f89369 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 23 Aug 2022 18:20:23 -0500 Subject: [PATCH 264/293] Suppressing Inaccessible Regions debug output --- DoorShuffle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index b973d2eb..fe24f6b8 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1896,9 +1896,9 @@ def find_inaccessible_regions(world, player): if any(x for x in ledge.exits if x.connected_region and x.connected_region.name == 'Agahnims Tower Portal'): world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') - logger.debug('Inaccessible Regions:') - for r in world.inaccessible_regions[player]: - logger.debug('%s', r) + #logger.debug('Inaccessible Regions:') + #for r in world.inaccessible_regions[player]: + # logger.debug('%s', r) def find_accessible_entrances(world, player, builder): From 56072e60940698c5a40fff98f45de276cacbb82d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 23 Aug 2022 18:39:05 -0500 Subject: [PATCH 265/293] Creating a separate copy_world_limited for OWR/ER purposes --- Main.py | 17 ++++++++++------- Rules.py | 15 +++++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Main.py b/Main.py index 0ebf52db..8288bb52 100644 --- a/Main.py +++ b/Main.py @@ -627,6 +627,7 @@ def copy_world_limited(world): create_dungeon_regions(ret, player) create_owedges(ret, player) create_shops(ret, player) + create_doors(ret, player) create_rooms(ret, player) create_dungeons(ret, player) @@ -658,13 +659,15 @@ def copy_world_limited(world): copiededge = ret.check_for_owedge(edge.name, edge.player) if copiededge is not None: copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) - # for door in world.doors: - # entrance = ret.check_for_entrance(door.name, door.player) - # if entrance is not None: - # destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) - # entrance.door = destdoor - # if destdoor is not None: - # destdoor.entrance = entrance + + for door in world.doors: + entrance = ret.check_for_entrance(door.name, door.player) + if entrance is not None: + destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) + entrance.door = destdoor + if destdoor is not None: + destdoor.entrance = entrance + ret.key_logic = world.key_logic.copy() from OverworldShuffle import categorize_world_regions diff --git a/Rules.py b/Rules.py index 260b2893..cc9bead3 100644 --- a/Rules.py +++ b/Rules.py @@ -341,7 +341,7 @@ def global_rules(world, player): set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Hookshot Ledge Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player)) - if not world.is_copied_world and not world.get_door('Ice Switch Room SE', player).entranceFlag: + if not world.get_door('Ice Switch Room SE', player).entranceFlag: set_rule(world.get_entrance('Ice Switch Room SE', 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)) @@ -398,7 +398,7 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('GT Conveyor Cross WN', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hookshot', player)) - if not world.is_copied_world and not world.get_door('GT Speed Torch SE', player).entranceFlag: + if not world.get_door('GT Speed Torch SE', player).entranceFlag: set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player)) @@ -421,7 +421,7 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Gauntlet 2 EN', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 2 SW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 3 NW', player), lambda state: state.can_kill_most_things(player)) - if not world.is_copied_world and not world.get_door('GT Gauntlet 3 SW', player).entranceFlag: + if not world.get_door('GT Gauntlet 3 SW', player).entranceFlag: set_rule(world.get_entrance('GT Gauntlet 3 SW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 4 NW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('GT Gauntlet 4 SW', player), lambda state: state.can_kill_most_things(player)) @@ -441,11 +441,10 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) # crystal switch rules - if not world.is_copied_world: - if world.get_door('Thieves Attic ES', player).crystal == CrystalBarrier.Blue: - set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) - else: - set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) + if world.get_door('Thieves Attic ES', player).crystal == CrystalBarrier.Blue: + set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) + else: + set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) set_rule(world.get_entrance('Thieves Attic Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) set_rule(world.get_entrance('Thieves Attic Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) From 0d9d6f2ceba7bfe2aaccbf2f8768d798902daaab Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 24 Aug 2022 12:53:04 -0600 Subject: [PATCH 266/293] Copy world needs to copy location type --- Main.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 3787e81b..bb2593be 100644 --- a/Main.py +++ b/Main.py @@ -549,11 +549,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) - # todo: this is potentially dangerous. later refactor so we - # can apply dynamic region rules on top of copied world like other rules - new_loc.access_rule = location.access_rule - new_loc.always_allow = location.always_allow - new_loc.item_rule = location.item_rule + new_loc.type = location.type new_reg.locations.append(new_loc) ret.clear_location_cache() From 5d419210afeb1ef9bb5e24743b24ff1be9e32c74 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 24 Aug 2022 13:18:01 -0600 Subject: [PATCH 267/293] Fix a bank conflict with OWR Release notes and version bump --- Main.py | 2 +- RELEASENOTES.md | 5 +++++ Rom.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Main.py b/Main.py index bb2593be..d47fbc46 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.1.1-u' +__version__ = '1.0.1.2-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 56ecca32..0603d95e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -183,6 +183,11 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable +* 1.0.1.2 + * Fixed an issue with small key bias rework + * Fixed an issue where trinity goal would open pyramid unexpectedly. (No longer does so if ER mdoe is shuffling holes). Crystals goal updated to match that behavior. + * Fixed a playthrough issue that was not respecting pot rules + * Fixed an issue that was conflicting with downstream OWR project * 1.0.1.1 * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be * Certain pot items no longer reload when reloading the supertile (matches original pot behavior better) diff --git a/Rom.py b/Rom.py index 58a144ad..4af847ad 100644 --- a/Rom.py +++ b/Rom.py @@ -1534,9 +1534,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(room.address(), room.rom_data()) if world.pottery[player] not in ['none']: - rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2A8000)) + rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2B8000)) # make hammer pegs use different tiles - Room0127.write_to_rom(snes_to_pc(0x2A8000), rom) + Room0127.write_to_rom(snes_to_pc(0x2B8000), rom) if world.pot_contents[player]: colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery'] From 0dfdbfd39ecfdbc2d6b93f30da58b874289f4c93 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 24 Aug 2022 15:00:48 -0600 Subject: [PATCH 268/293] Removed "good bee" from Mothula logic Fixed an issue with Mystery generation and windows file path --- BaseClasses.py | 20 ++++++++++---------- Bosses.py | 3 +-- Mystery.py | 12 ++++++------ RELEASENOTES.md | 2 ++ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7f6a00c6..36758575 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1093,16 +1093,16 @@ class CollectionState(object): return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player)) return self.has('Bow', player) - def can_get_good_bee(self, player): - cave = self.world.get_region('Good Bee Cave', player) - return ( - self.can_use_bombs(player) and - self.has_bottle(player) and - self.has('Bug Catching Net', player) and - (self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and - cave.can_reach(self) and - self.is_not_bunny(cave, player) - ) + # def can_get_good_bee(self, player): + # cave = self.world.get_region('Good Bee Cave', player) + # return ( + # self.can_use_bombs(player) and + # self.has_bottle(player) and + # self.has('Bug Catching Net', player) and + # (self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and + # cave.can_reach(self) and + # self.is_not_bunny(cave, player) + # ) def has_sword(self, player): return self.has('Fighter Sword', player) or self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player) diff --git a/Bosses.py b/Bosses.py index 206b797c..d84df921 100644 --- a/Bosses.py +++ b/Bosses.py @@ -61,8 +61,7 @@ def MothulaDefeatRule(state, player): # TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply # to non-vanilla locations, so are harder to test, so sticking with what VT has for now: (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or - (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or - state.can_get_good_bee(player) + (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) ) def BlindDefeatRule(state, player): diff --git a/Mystery.py b/Mystery.py index 20e0e2ca..568ff6a8 100644 --- a/Mystery.py +++ b/Mystery.py @@ -1,5 +1,7 @@ import argparse import logging +from pathlib import Path +import os import RaceRandom as random import urllib.request import urllib.parse @@ -104,13 +106,11 @@ def main(): DRMain(erargs, seed, BabelFish()) def get_weights(path): - try: - if urllib.parse.urlparse(path).scheme: - return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) - with open(path, 'r', encoding='utf-8') as f: + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: return yaml.load(f, Loader=yaml.SafeLoader) - except Exception as e: - raise Exception(f'Failed to read weights file: {e}') + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) def roll_settings(weights): def get_choice(option, root=None): diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0603d95e..dc790ea1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,6 +184,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable * 1.0.1.2 + * Removed "good bee" as an in-logic way of killing Mothula + * Fixed an issue with Mystery generation and Windows path * Fixed an issue with small key bias rework * Fixed an issue where trinity goal would open pyramid unexpectedly. (No longer does so if ER mdoe is shuffling holes). Crystals goal updated to match that behavior. * Fixed a playthrough issue that was not respecting pot rules From 01de92d10f741f3bb4809b425208de52ed1d618d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 25 Aug 2022 00:27:38 -0500 Subject: [PATCH 269/293] Chickens now spawn on same layer as Link --- Rom.py | 2 +- data/base2current.bps | Bin 104377 -> 104378 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 7b437caa..9d565be5 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '94a8df9d13105abcd655c7d59294ad20' +RANDOMIZERBASEHASH = '831beb6f60c3c99467552493b3ce6f19' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index af312a912f5341731add985f82cf58223b65bc3a..b7f58d1854a76692e9455cf8918f0f71df013f5d 100644 GIT binary patch delta 806 zcmWlVZ)g*D7{~eLF6l|y+S=AM>0evhnks2cYYk$t+9XX_nu?oCjhUPCU*K@7pop!e z&$UJkZM?aMdaXA}@sM)sDYu*Zj^V4LfycSMw=%&q|>t2<_CSGGb3Y74hM^ zJv{MYYR|u%?~)OxOvI-j@AVPW%lS9kOz$q;hSXo~or#!Yk#@?(1>?fm7U--VpMsAJ zownU*^#3aC`s2^*9oBE=Ca0j%_>pqb;V4!Fx|uWQCvbltS#tV{5v9f2NItb#rqj$X3}w~TG&9fRIs7=(OaeF@sv*rd9Xd%*rt>dGHaW~Oo$6f zPJ9^ujr-aQ>5e3}>NgIGq$u14GejhNN5Lk4qIZ3^RL(Vd1Od=KqrW`WS?uWkO1Qtm z8kU7MNwsj6B%DvI$?edi_rRC^){?Ccr3B6`-?XqBF!-pI4C$vH`2$58n!mkvA^yW( X7JRJiTzXJOR>lkd|Hpa%TXCsLFeC8$B^meen z&TzTkXN~t3st*Y*)V4r@XBFkoz5-i3;hY2K)F83Sk*R3LAhn1UFyV2SyAOWpohG#8 z7dzmqrG(>`i^RJuHTJ93gd4uprsHAmZ6n!Gq_%7egm1H6FHTfmQfmY z!}Ip`T{G=C@tfGefs?uN)+u4rY|NSX*zLfq`;wkj|BxmzT!2%@i!f*AV+k!5FZNN= zI#!(_RN3)3B9z3is!kgoR`RmyG^r9PoI-{F0e&S!I_5aLHLHb>@fDhCegVEuNiJ5Z zWLYEQ@#b+=@TkK*?~uUdrL9F+yqIX0!@}M8#*y@Ct(zjklx}=*Bd}mJkUOgGAcbRA2Oi#$OzhO2QMM2#DQyK>9c~LB6Ya^Q+C^rkEemmC zmE1Q}2{oZ@$Og$!b*{%!2Wh+BDN+1!AyA|7ox zg^gmD(ztyrpRQfPaTrbwDBlj%TW$UW+_Q{c)s19@rR*DMThV7jxDf}GHF)=WgTD5) x5QaPB*LBC{Li0o=I-wk%U_6$^ Date: Thu, 25 Aug 2022 00:30:32 -0500 Subject: [PATCH 270/293] Changed Castle Gate to only require mirror when screen not swapped --- Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rules.py b/Rules.py index cc9bead3..0930270b 100644 --- a/Rules.py +++ b/Rules.py @@ -872,8 +872,6 @@ def default_rules(world, player): set_rule(world.get_entrance('Potion Shop Rock (North)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Zora Approach Rocks (West)', player), lambda state: state.can_lift_heavy_rocks(player) or state.has_Boots(player)) set_rule(world.get_entrance('Zora Approach Rocks (East)', player), lambda state: state.can_lift_heavy_rocks(player) or state.has_Boots(player)) - set_rule(world.get_entrance('Hyrule Castle Main Gate (South)', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hyrule Castle Inner East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Hyrule Castle Outer East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bat Cave Ledge Peg', player), lambda state: state.has('Hammer', player)) @@ -1128,6 +1126,8 @@ def ow_inverted_rules(world, player): set_rule(world.get_entrance('HC East Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Courtyard Left Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Area South Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Hyrule Castle Main Gate (South)', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has_beaten_aga(player)) set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has_beaten_aga(player)) else: From 55364c071af640cc1b064b36ec9a05df6b4557a2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 26 Aug 2022 14:42:06 -0600 Subject: [PATCH 271/293] Fixed issue with inverted and certain pottery settings. --- BaseClasses.py | 5 +++++ RELEASENOTES.md | 1 + Regions.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 36758575..12ef4ee3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2805,6 +2805,11 @@ class Pot(object): item = self.item if not self.indicator else self.standing_item_code return [self.x, high_byte, item] + def get_region(self, world, player): + if world.mode[player] == 'inverted' and self.room == 'Links House': + return world.get_region('Inverted Links House', 1) + return world.get_region(self.room, 1) + def __eq__(self, other): return self.x == other.x and self.y == other.y and self.room == other.room diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dc790ea1..76df07c1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -190,6 +190,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Fixed an issue where trinity goal would open pyramid unexpectedly. (No longer does so if ER mdoe is shuffling holes). Crystals goal updated to match that behavior. * Fixed a playthrough issue that was not respecting pot rules * Fixed an issue that was conflicting with downstream OWR project + * Fixed an issue with inverted and certain pottery settings * 1.0.1.1 * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be * Certain pot items no longer reload when reloading the supertile (matches original pot behavior better) diff --git a/Regions.py b/Regions.py index c0ef0364..31a5ab92 100644 --- a/Regions.py +++ b/Regions.py @@ -1065,9 +1065,9 @@ def valid_pot_location(pot, pot_set, world, player): return True if world.pottery[player] in ['reduced', 'clustered'] and pot in pot_set: return True - if world.pottery[player] == 'dungeon' and world.get_region(pot.room, player).type == RegionType.Dungeon: + if world.pottery[player] == 'dungeon' and pot.get_region(world, player).type == RegionType.Dungeon: return True - if world.pottery[player] in ['cave', 'cavekeys'] and world.get_region(pot.room, player).type == RegionType.Cave: + if world.pottery[player] in ['cave', 'cavekeys'] and pot.get_region(world, player).type == RegionType.Cave: return True return False From 3e6f2bb79be31ad26bc949aa56b39b34018ccc45 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 26 Aug 2022 15:20:53 -0600 Subject: [PATCH 272/293] Fixed issue with small key shuffle when big keys aren't --- Fill.py | 3 +++ RELEASENOTES.md | 1 + 2 files changed, 4 insertions(+) diff --git a/Fill.py b/Fill.py index b2dc3400..e2ea4726 100644 --- a/Fill.py +++ b/Fill.py @@ -48,6 +48,9 @@ def fill_dungeons_restrictive(world, shuffled_locations): bigs, smalls, others = [], [], [] for i in dungeon_items: (bigs if i.bigkey else smalls if i.smallkey else others).append(i) + for i in world.itempool: + if i.smallkey and world.keyshuffle[i.player]: + smalls.append(i) def fill(base_state, items, key_pool): fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 76df07c1..1e059997 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -191,6 +191,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Fixed a playthrough issue that was not respecting pot rules * Fixed an issue that was conflicting with downstream OWR project * Fixed an issue with inverted and certain pottery settings + * Fixed an issue with small keys being shuffled and big keys not (key distribution) * 1.0.1.1 * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be * Certain pot items no longer reload when reloading the supertile (matches original pot behavior better) From ae0efad8307158d1b320227835659b5b3f0c5e21 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 26 Aug 2022 15:48:07 -0600 Subject: [PATCH 273/293] More inverted pot fixes --- PotShuffle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PotShuffle.py b/PotShuffle.py index 7806f495..a2a10c0f 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -894,7 +894,7 @@ def shuffle_pots(world, player): new_pot.item = PotItem.FiveRupees if new_pot.item == PotItem.Key: - key = next(location for location in world.get_region(old_pot.room, player).locations if location.name in key_drop_data) + key = next(location for location in old_pot.get_region(world, player).locations if location.name in key_drop_data) key.pot = new_pot if new_pot.room != old_pot.room: # Move pot key to new room @@ -970,7 +970,7 @@ def choose_pots(world, player): dungeon_list = [] for super_tile, pot_list in vanilla_pots.items(): for pot in pot_list: - if world.get_region(pot.room, player).type == RegionType.Cave: + if pot.get_region(world, player).type == RegionType.Cave: pot_pool.add(pot) else: dungeon_list.append(pot) @@ -981,7 +981,7 @@ def choose_pots(world, player): dungeon_count = 0 for super_tile, pot_list in vanilla_pots.items(): for pot in pot_list: - if world.get_region(pot.room, player).type == RegionType.Cave: + if pot.get_region(world, player).type == RegionType.Cave: pot_pool.add(pot) else: dungeon_map[pot.room].append(pot) From 45395e9b15e67e8062e6df1b0eabcf1784d9b98d Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 26 Aug 2022 16:08:18 -0600 Subject: [PATCH 274/293] Legacy pot shuffle fix --- Rules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Rules.py b/Rules.py index c7b9ef44..a553fbfe 100644 --- a/Rules.py +++ b/Rules.py @@ -322,8 +322,10 @@ def global_rules(world, player): # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) loc = world.get_location('Misery Mire - Spikes Pot Key', player) - if loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area - set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) + if loc.pot: + if loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area + set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) + or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player)) From 4aff460b22b1fe1af0ec9997b3d447b1aaa4f1d9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 30 Aug 2022 13:51:12 -0600 Subject: [PATCH 275/293] Fix for crossed doors with ambrosia Fix for ER + OWG in crossed doors Fix for Small Key shuffle --- DoorShuffle.py | 11 ++++++++--- EntranceShuffle.py | 5 +++-- Fill.py | 7 ++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index ecb1b503..380193d5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -988,11 +988,16 @@ def cross_dungeon(world, player): paths = determine_required_paths(world, player) check_required_paths(paths, world, player) + hc_compass = ItemFactory('Compass (Escape)', player) + at_compass = ItemFactory('Compass (Agahnims Tower)', player) + at_map = ItemFactory('Map (Agahnims Tower)', player) + if world.restrict_boss_items[player] != 'none': + hc_compass.advancement = at_compass.advancement = at_map.advancement = True hc = world.get_dungeon('Hyrule Castle', player) - hc.dungeon_items.append(ItemFactory('Compass (Escape)', player)) + hc.dungeon_items.append(hc_compass) at = world.get_dungeon('Agahnims Tower', player) - at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player)) - at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player)) + at.dungeon_items.append(at_compass) + at.dungeon_items.append(at_map) assign_cross_keys(dungeon_builders, world, player) all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 82aa6a75..f67c089b 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -63,9 +63,10 @@ def link_entrances(world, player): connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) elif world.doorShuffle[player] != 'vanilla': # sanc is in light world, so must all of HC if door shuffle is on - connect_mandatory_exits(world, lw_entrances, - [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], + hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')] + connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) + connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) else: connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) diff --git a/Fill.py b/Fill.py index e2ea4726..5a414c98 100644 --- a/Fill.py +++ b/Fill.py @@ -48,9 +48,10 @@ def fill_dungeons_restrictive(world, shuffled_locations): bigs, smalls, others = [], [], [] for i in dungeon_items: (bigs if i.bigkey else smalls if i.smallkey else others).append(i) + unplaced_smalls = list(smalls) for i in world.itempool: if i.smallkey and world.keyshuffle[i.player]: - smalls.append(i) + unplaced_smalls.append(i) def fill(base_state, items, key_pool): fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) @@ -59,12 +60,12 @@ def fill_dungeons_restrictive(world, shuffled_locations): big_state_base = all_state_base.copy() for x in smalls + others: big_state_base.collect(x, True) - fill(big_state_base, bigs, smalls) + fill(big_state_base, bigs, unplaced_smalls) random.shuffle(shuffled_locations) small_state_base = all_state_base.copy() for x in others: small_state_base.collect(x, True) - fill(small_state_base, smalls, list(smalls)) + fill(small_state_base, smalls, unplaced_smalls) random.shuffle(shuffled_locations) fill(all_state_base, others, None) From 258d66fb5cef6c5c477a1eac4e668fba41411f2c Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 31 Aug 2022 14:10:02 -0600 Subject: [PATCH 276/293] Minor standard fix, fix for test suite, made one error less annoying --- DoorShuffle.py | 6 +++++- TestSuite.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 380193d5..ea287d8f 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -214,7 +214,7 @@ def vanilla_key_logic(world, player): key_layout = build_key_layout(builder, start_regions, doors, world, player) valid = validate_key_layout(key_layout, world, player) if not valid: - logging.getLogger('').warning('Vanilla key layout not valid %s', builder.name) + logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) builder.key_door_proposal = doors if player not in world.key_logic.keys(): world.key_logic[player] = {} @@ -1911,8 +1911,10 @@ def find_inaccessible_regions(world, player): def find_accessible_entrances(world, player, builder): entrances = [region.name for region in (portal.door.entrance.parent_region for portal in world.dungeon_portals[player]) if region.dungeon.name == builder.name] entrances.extend(drop_entrances[builder.name]) + hc_std = False if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle': + hc_std = True start_regions = ['Hyrule Castle Courtyard'] elif world.mode[player] != 'inverted': start_regions = ['Links House', 'Sanctuary'] @@ -1937,6 +1939,8 @@ def find_accessible_entrances(world, player, builder): if connect not in queue and connect not in visited_regions: queue.append(connect) for ext in next_region.exits: + if hc_std and ext.name == 'Hyrule Castle Main Gate (North)': # just skip it + continue connect = ext.connected_region if connect is None or ext.door and ext.door.blocked: continue diff --git a/TestSuite.py b/TestSuite.py index 062cb2eb..ca620b21 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -45,7 +45,7 @@ def main(args=None): test("Vanilla ", "--shuffle vanilla") test("Retro ", "--retro --shuffle vanilla") - test("Keysanity ", "--shuffle vanilla --keydropshuffle drops_only --keysanity") + test("Keysanity ", "--shuffle vanilla --dropshuffle --keysanity") test("Shopsanity", "--shuffle vanilla --shopsanity") test("Simple ", "--shuffle simple") test("Full ", "--shuffle full") From 2c7817057918e66dc3287397dd7e13e95223ca44 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 31 Aug 2022 16:16:20 -0600 Subject: [PATCH 277/293] Pyinstaller 5.0+ build fix --- source/meta/build-dr.py | 1 - source/meta/build-gui.py | 1 - 2 files changed, 2 deletions(-) diff --git a/source/meta/build-dr.py b/source/meta/build-dr.py index 6f26adb9..a83c9d56 100644 --- a/source/meta/build-dr.py +++ b/source/meta/build-dr.py @@ -22,7 +22,6 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform. subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", upx_string, "-y ", - "--onefile ", f"--distpath {DEST_DIRECTORY} ", ]), shell=True) diff --git a/source/meta/build-gui.py b/source/meta/build-gui.py index ae284261..4986df67 100644 --- a/source/meta/build-gui.py +++ b/source/meta/build-gui.py @@ -22,7 +22,6 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform. subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", upx_string, "-y ", - "--onefile ", f"--distpath {DEST_DIRECTORY} ", ]), shell=True) From c10a791ed6bc761642726709aa92ab4e11dd94a2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 2 Sep 2022 13:10:03 -0600 Subject: [PATCH 278/293] Important key fix for logic placment --- Fill.py | 3 ++- KeyDoorShuffle.py | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index 5a414c98..d7a97bf2 100644 --- a/Fill.py +++ b/Fill.py @@ -172,7 +172,8 @@ def valid_key_placement(item, location, key_pool, world): if key_logic.prize_location: prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] - return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) + wild_keys = world.keyshuffle[item.player] != 'none' + return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index bb4f6835..ad06f3fc 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -62,9 +62,9 @@ class KeyLogic(object): self.sm_doors = {} self.prize_location = None - def check_placement(self, unplaced_keys, big_key_loc=None, prize_loc=None, cr_count=7): + def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): + if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -186,17 +186,20 @@ class PlacementRule(object): if not bk_blocked and check_locations is None: return True available_keys = outside_keys - empty_chests = 0 # todo: sometimes we need an extra empty chest to accomodate the big key too # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk - for loc in check_locations: - if not loc.item: - empty_chests += 1 - elif loc.item and loc.item.name == self.small_key: - available_keys += 1 - place_able_keys = min(empty_chests, unplaced_keys) - available_keys += place_able_keys + if not wild_keys: + empty_chests = 0 + for loc in check_locations: + if not loc.item: + empty_chests += 1 + elif loc.item and loc.item.name == self.small_key: + available_keys += 1 + place_able_keys = min(empty_chests, unplaced_keys) + available_keys += place_able_keys + else: + available_keys += unplaced_keys return available_keys >= threshold From 9cca25bdc4c7b1d96f7a974545e1fa9d22cfb933 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 2 Sep 2022 13:11:03 -0600 Subject: [PATCH 279/293] Modified key fix for Unstable --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index d7a97bf2..23058688 100644 --- a/Fill.py +++ b/Fill.py @@ -172,7 +172,7 @@ def valid_key_placement(item, location, key_pool, world): if key_logic.prize_location: prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] - wild_keys = world.keyshuffle[item.player] != 'none' + wild_keys = world.keyshuffle[item.player] return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) From 43130d05786aed47475fe8453b0f79cb01b5b492 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:41:25 +0200 Subject: [PATCH 280/293] Fix tile swap parity check in Keep Similar --- OverworldShuffle.py | 65 ++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 289bcf20..6cff5c22 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -561,8 +561,8 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): group_parity = {} for group_data in groups: group = group_data[0] - parity = [0, 0, 0, 0, 0] - # vertical land + parity = [0, 0, 0, 0, 0, 0] + # 0: vertical if 0x00 in group: parity[0] += 1 if 0x0f in group: @@ -571,40 +571,45 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): parity[0] -= 1 if 0x81 in group: parity[0] -= 1 - # horizontal land + # 1: horizontal land single if 0x1a in group: parity[1] -= 1 if 0x1b in group: parity[1] += 1 if 0x28 in group: - parity[1] += 1 - if 0x29 in group: parity[1] -= 1 - if 0x30 in group: - parity[1] -= 2 - if 0x3a in group: - parity[1] += 2 - # horizontal water - if 0x2d in group: + if 0x29 in group: + parity[1] += 1 + # 2: horizontal land double + if 0x28 in group: parity[2] += 1 - if 0x80 in group: + if 0x29 in group: parity[2] -= 1 - # whirlpool + if 0x30 in group: + parity[2] -= 1 + if 0x3a in group: + parity[2] += 1 + # 3: horizontal water + if 0x2d in group: + parity[3] += 1 + if 0x80 in group: + parity[3] -= 1 + # 4: whirlpool if 0x0f in group: - parity[3] += 1 + parity[4] += 1 if 0x12 in group: - parity[3] += 1 + parity[4] += 1 if 0x33 in group: - parity[3] += 1 + parity[4] += 1 if 0x35 in group: - parity[3] += 1 - # dropdown exit + parity[4] += 1 + # 5: dropdown exit if 0x00 in group or 0x02 in group or 0x13 in group or 0x15 in group or 0x18 in group or 0x22 in group: - parity[4] += 1 + parity[5] += 1 if 0x1b in group and world.mode[player] != 'standard': - parity[4] += 1 + parity[5] += 1 if 0x1b in group and world.shuffle_ganon: - parity[4] -= 1 + parity[5] -= 1 group_parity[group[0]] = parity attempts = 1000 @@ -629,18 +634,24 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): exist_lw_regions.extend(lw_regions) exist_dw_regions.extend(dw_regions) - parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(5)] - parity[3] %= 2 # actual parity - if (world.owCrossed[player] == 'none' or do_grouped) and parity[:4] != [0, 0, 0, 0]: + parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)] + if not world.owKeepSimilar[player]: + parity[1] += 2*parity[2] + parity[2] = 0 + # if crossed terrain: + # parity[1] += parity[3] + # parity[3] = 0 + parity[4] %= 2 # actual parity + if (world.owCrossed[player] == 'none' or do_grouped) and parity[:5] != [0, 0, 0, 0, 0]: attempts -= 1 continue # ensure sanc can be placed in LW in certain modes if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): - free_dw_drops = parity[4] + (1 if world.shuffle_ganon else 0) + free_dw_drops = parity[5] + (1 if world.shuffle_ganon else 0) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon else 0) if free_dw_drops == free_drops: - attempts -= 1 - continue + attempts -= 1 + continue break (exist_owids, exist_lw_regions, exist_dw_regions) = result_list From 2bd89bc367e583bb2910f21e2f9e745c15f5aa1c Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:47:05 +0200 Subject: [PATCH 281/293] Fix for Digging Game screens in Grouped --- OverworldShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6cff5c22..17a8faf0 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -727,7 +727,7 @@ def define_tile_groups(world, player, do_grouped): if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) - if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and world.owCrossed[player] == 'none': + if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x28, 0x29]]) if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped): From d6ae8b9fab6d2f9ade98748f8553ef118a55e9f1 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:51:59 +0200 Subject: [PATCH 282/293] Counting is hard --- OverworldShuffle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 17a8faf0..fe417281 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -604,8 +604,9 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): if 0x35 in group: parity[4] += 1 # 5: dropdown exit - if 0x00 in group or 0x02 in group or 0x13 in group or 0x15 in group or 0x18 in group or 0x22 in group: - parity[5] += 1 + for id in [0x00, 0x02, 0x13, 0x15, 0x18, 0x22]: + if id in group: + parity[5] += 1 if 0x1b in group and world.mode[player] != 'standard': parity[5] += 1 if 0x1b in group and world.shuffle_ganon: From a9ed28c59adac2dc86a6c9641ceab43d407fb97a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 4 Sep 2022 14:09:54 -0500 Subject: [PATCH 283/293] Overhauled how map checks display dungeon prizes --- Rom.py | 55 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/Rom.py b/Rom.py index f028b443..598df466 100644 --- a/Rom.py +++ b/Rom.py @@ -1484,8 +1484,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] 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] and not (world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default')): + 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 @@ -1500,39 +1499,49 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): 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.shuffle[player] == 'vanilla': + elif world.overworld_map[player] == 'default': # disable HC/AT/GT icons - # rom.write_bytes(0x53E8A, int16_as_bytes(0xFF00)) # GT - # rom.write_bytes(0x53E8C, int16_as_bytes(0xFF00)) # AT + 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': - if len(portal_list) == 1: - portal_idx = 0 - else: - if world.doorShuffle[player] == 'crossed': - # 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] + 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) else: - if dungeon in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: - portal_idx = -1 - elif len(portal_list) == 1: - portal_idx = 0 + if world.shuffle[player] != 'vanilla': + if len(portal_list) == 1: + portal_idx = 0 + else: + if world.doorShuffle[player] == 'crossed': + # 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] 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() + 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.shuffle[player] == 'vanilla' or world.overworld_map[player] != 'default': + 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) + # in crossed doors - flip the compass exists flags if world.doorShuffle[player] == 'crossed': for dungeon, portal_list in dungeon_portals.items(): From 0030e1bf1d9174e4c1ef03f5d34ca9be2b8c0108 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 4 Sep 2022 22:23:35 -0500 Subject: [PATCH 284/293] Version bump 0.2.10.0 --- CHANGELOG.md | 14 ++++++++++++++ OverworldShuffle.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1755913e..aac5994d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.2.10.0 +- Merged DR v1.0.1.1-1.0.1.2 + - Removed text color from hint tiles + - Removed Good Bee requirement from Mothula + - Some keylogic/generation fixes + - Fixed a Pottery logic issue in the playthru +- Fixed a generation error in Mixed OWR, resulting in more possible Mixed scenarios (thanks Catobat) +- Added more scenarios where OW Map Checks in Mixed OWR show dungeon prizes in their respective worlds +- Fixed rupee logic to consider Pottery option and lack of early rupees +- Changed Lean ER + Inverted Dark Chapel start is guaranteed to be in DW +- Fixed graphical issue with Hammerpeg Cave +- Fixed logic rule with HC Main Gate to not require mirror if screen is swapped +- Removed Crossed OWR option: "None (Allowed)" + ### 0.2.9.1 - Lite/Lean ER now includes Cave Pot locations with various Pottery options - Changed Unique Boss Shuffle so that GT Bosses are unique amongst themselves diff --git a/OverworldShuffle.py b/OverworldShuffle.py index fe417281..bf1193a4 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.9.1' +version_number = '0.2.10.0' # branch indicator is intentionally different across branches version_branch = '-u' From 9a62841557e35c353f1b92138a6f375ccbabdd57 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 5 Sep 2022 12:46:03 -0500 Subject: [PATCH 285/293] More descriptive error for failed dungeon prize placement --- ItemList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ItemList.py b/ItemList.py index 5c4aa40d..29ef71da 100644 --- a/ItemList.py +++ b/ItemList.py @@ -545,7 +545,7 @@ def fill_prizes(world, attempts=15): continue break else: - raise FillError('Unable to place dungeon prizes') + raise FillError("Unable to place dungeon prizes: {}".format(", ".join(list(map(lambda d: d.hint_text, prize_locs))))) def set_up_shops(world, player): From f74a64d0b8cca257f70b0e9abfb5f3b02851b766 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 5 Sep 2022 12:46:56 -0500 Subject: [PATCH 286/293] Avoid bunny rules in limited copy world --- Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index 6b8812fd..d18c9ae5 100644 --- a/Rules.py +++ b/Rules.py @@ -67,7 +67,8 @@ def set_rules(world, player): if not world.swamp_patch_required[player]: add_rule(world.get_entrance('Swamp Lobby Moat', player), lambda state: state.has_Mirror(player)) - set_bunny_rules(world, player, world.mode[player] == 'inverted') + if not world.is_copied_world: + set_bunny_rules(world, player, world.mode[player] == 'inverted') def mirrorless_path_to_location(world, startName, targetName, player): From c298b392d003e778d5c56d17bd54b5687dc6370b Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Sep 2022 16:53:52 -0600 Subject: [PATCH 287/293] Fix for rain prevented doors fouling up key doors --- Main.py | 2 +- RELEASENOTES.md | 3 +++ Rom.py | 2 +- data/base2current.bps | Bin 93186 -> 93219 bytes 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index d47fbc46..2f7350c5 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.1.2-u' +__version__ = '1.0.1.3-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1e059997..dcb80a60 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -183,6 +183,9 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable +* 1.0.1.3 + * Fix for rain prevented doors fouling up key doors + * Couple minor issues * 1.0.1.2 * Removed "good bee" as an in-logic way of killing Mothula * Fixed an issue with Mystery generation and Windows path diff --git a/Rom.py b/Rom.py index 4af847ad..b76cfef8 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '0f96237c73cccaf7a250343fe3e8c887' +RANDOMIZERBASEHASH = 'fc4d4a01f8c4e00280ea5640297f8e9c' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 55525ee50a0d8f30527d68ac4de47956a9acb906..25cc58b2c300e723aaa269298be9cd15e9323ea3 100644 GIT binary patch delta 1046 zcmW-dZ%i9?7{~9q{)HB?Q?WC(W!#2$Kb7H74*vL*`B8QaBU$F0f9mO*C*SYq`+T1l zk9SQqv8KA#2>K-_sZ$X^lrVRC6UU)K0fh;l@(WUn`Uh>pe!C&@f+iDSdZJOQgr3Ap z`l}kapCHZ0E1)Cc+S^e`YD0N>UnCr9A|>b5?wFSF#4q3@UBXsH^q=(R=7J>IQ|Yu+v=mv{+CU2rCHoSwq6>DP z<*qLxca>)m(QEE}Pm~yK?BQZ$%pjAL4OaAHo}Rz<(TWY^Y2850r~bC2)RJzV$dc4< z_k*iGk37Hjd2Xa74O$CCNLqnPg;ZL~A3aJ1{*bW>b@MX3&Le`Wa^<`JN78@jL|~!D znnyF>pB&uHqg}$-u&7`o=hQd5aYr_6)Ae!a33kjflQ7O=J{v53*4ghba-ssA`M`xP zaUwz=bB;O!ZE=EyW^8^hg9PvVqEpn$k6;OxBMnu*61Z#j;)%m_P`P_}OInhXNs&OO z1(p;wvXRx4sB%!mn$K+GW zQ2YSxbm%eB&&G)F%?vxU zThw88(OEcTIK|)&L(-88^%;tW+8O+=70MKCbu-B^D`W#%b_B<_!#?UP;p5#ko=7Ba edWH)(CDYr11gK4t#qg+J`>plp;cFAeKKT~_?x1@B delta 1033 zcmW-de@q*77{>3t_Sym+*V4j@VE3i`Xd5(wie+0EE^3U`ty8n9IJYL72o{hyBVY*J zmEA(AU4LB47url(*hM*|xYiUt5uVl00=EYd%{whlUC7KtB47Gq- z>Rk(nx!z)MjT4bO2{NIPc*O)6^_Z*opL?=Z#f~bzH_yz; ztxP6IkaK`+1I#8tKGu6Ij1C9TW87Htx5;;>PizDXQh-;-@k~Bw3@y`_j|2o6I&#d3 z+yMch&jZ%y3G@rTnGeS2cFr6>>|loF4dMIZqV$eQfk1nCjuey1hrrCjDDW-S23P5JQGsgJZ+b*VXisn z2|~2^FRm~&bwW{IU!#usCkf~*^;chLwZE3>nv$(O{ z!v=6Oi+eq6{e(Nd?Lr@8GZhbxM?0OKsIAQtHQ{wFm`Fz)mli;?wt2)qj!o^%cPB6O z&DDsg8S@1YDllSR;}(=eM)sN!C$yHSTx`<;Lc?LRq5o{yJehWi=Fu06mX{L`Dx^Bm zWfS8nv=evfpt7jSqGzJJ23R`!W_~0nDp2>xS05JOvpQ(3-aj1O%J`(cckv*r#%*pc zQI`6^?foKnyU16uc_LoUgnSCLj?-V`GCdT+ZEVwnvA79aK6~i8nuZbcDO5Wm$ss%c zgqwdEcj&Yf3NjZh`C_YI`xjgSvyrvqoUz)t!l!V_%S=8DCpbWaDOrW8Uz0#K>) RGFZ2(mZ`;_)b2~~{0GNjp9KH_ From 4ce85a9f2db4722c14882945e56f2af5cdde0505 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Sep 2022 16:49:47 -0600 Subject: [PATCH 288/293] Couple minor issues - let a door not be dead in higher intensity, slightly better reporting --- Doors.py | 7 ++++++- ItemList.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Doors.py b/Doors.py index 26d474cc..a7c1d9f3 100644 --- a/Doors.py +++ b/Doors.py @@ -435,7 +435,7 @@ def create_doors(world, player): create_door(player, 'PoD Dark Basement W Up Stairs', Sprl).dir(Up, 0x6a, 0, HTH).ss(S, 0x1b, 0x3c, True), create_door(player, 'PoD Dark Basement E Up Stairs', Sprl).dir(Up, 0x6a, 1, HTH).ss(S, 0x1b, 0x9c, True), create_door(player, 'PoD Dark Alley NE', Nrml).dir(No, 0x6a, Right, High).big_key().pos(0), - create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1).kill().portal(Z, 0x00), + create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1).portal(Z, 0x00), create_door(player, 'PoD Mimics 2 NW', Intr).dir(No, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue SW', Intr).dir(So, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue Left to Right Barrier - Orange', Lgcl), @@ -1467,6 +1467,11 @@ def create_doors(world, player): world.get_door('GT Spike Crystal Right to Left Barrier - Orange', player).barrier(CrystalBarrier.Orange) world.get_door('GT Spike Crystal Left to Right Bypass', player).barrier(CrystalBarrier.Blue) + # kill certain doors + if world.intensity[player] == 1: # due to ladder & warp being fixed + world.get_door('PoD Mimics 2 SW', player).kill() + + # nifty dynamic logical doors: south_controller = world.get_door('Ice Cross Bottom SE', player) east_controller = world.get_door('Ice Cross Right ES', player) diff --git a/ItemList.py b/ItemList.py index 9dd69157..279fec61 100644 --- a/ItemList.py +++ b/ItemList.py @@ -514,7 +514,7 @@ def fill_prizes(world, attempts=15): continue break else: - raise FillError('Unable to place dungeon prizes') + raise FillError(f'Unable to place dungeon prizes {", ".join(list(map(lambda d: d.hint_text, prize_locs)))}') def set_up_shops(world, player): From 1e4b857dabead4e1e6d29574a7d19d373c29b64f Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Sep 2022 16:55:46 -0600 Subject: [PATCH 289/293] Update default setting to vanilla --- CLI.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CLI.py b/CLI.py index ba5090ec..e212c935 100644 --- a/CLI.py +++ b/CLI.py @@ -174,12 +174,12 @@ def parse_settings(): "keyshuffle": False, "bigkeyshuffle": False, "keysanity": False, - "door_shuffle": "basic", - "intensity": 2, - "experimental": False, - "dungeon_counters": "default", - "mixed_travel": "prevent", - "standardize_palettes": "standardize", + 'door_shuffle': 'vanilla', + 'intensity': 2, + 'experimental': False, + 'dungeon_counters': 'default', + 'mixed_travel': 'prevent', + 'standardize_palettes': 'standardize', "triforce_pool": 0, "triforce_goal": 0, From 3227ffee1439390671917631b38cfde1674f7915 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Sep 2022 21:30:57 -0600 Subject: [PATCH 290/293] Missed a change --- KeyDoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index ad06f3fc..b600f814 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -158,7 +158,7 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count): if self.prize_relevance and prize_location: if self.prize_relevance == 'BigBomb': if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: From 7adb75cfb96dafb8e762579d37e8f56d3e6ca751 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 8 Sep 2022 13:50:38 -0500 Subject: [PATCH 291/293] Added Unique Boss Shuffle to mystery example yaml --- mystery_example.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mystery_example.yml b/mystery_example.yml index ab7ffe63..1093ec75 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -154,6 +154,7 @@ none: 3 simple: 1 full: 1 + unique: 1 random: 1 enemy_shuffle: none: 3 From f393bad008e1f659bf91731833401671c8cf11c8 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 9 Sep 2022 20:40:37 -0500 Subject: [PATCH 292/293] Added Menu Speed to example mystery yaml --- mystery_example.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mystery_example.yml b/mystery_example.yml index 1093ec75..feb4a3ed 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -203,6 +203,13 @@ half: 0 quarter: 1 off: 0 + menuspeed: + normal: 1 + double: 0 + triple: 0 + quadruple: 0 + instant: 0 + half: 0 shuffle_sfx: on: 1 off: 1 From 635fd10a8ce8a12a41940401efc22d835b89ecd4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 18 Sep 2022 17:55:17 -0500 Subject: [PATCH 293/293] Version bump 0.2.10.1 --- CHANGELOG.md | 6 ++++++ OverworldShuffle.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aac5994d..d0a059de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.10.1 +- Merged DR v1.0.1.3 + - Fixed Zelda despawn in TT Prison + - Fixed issue with key door usage in rainstate +- Added missing modes to example mystery yaml + ## 0.2.10.0 - Merged DR v1.0.1.1-1.0.1.2 - Removed text color from hint tiles diff --git a/OverworldShuffle.py b/OverworldShuffle.py index bf1193a4..f881aa81 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.10.0' +version_number = '0.2.10.1' # branch indicator is intentionally different across branches version_branch = '-u'