SM: 20221101 update (#1479)

This adds support to most of Varia's 20221101 update. Notably, added Options for:
- Objectives
- Tourian
- RelaxedRoundRobinCF

As well as previously unsupported Options:
- EscapeRando
- RemoveEscapeEnemies
- HideItems
This commit is contained in:
lordlou 2023-04-09 18:35:46 -04:00 committed by GitHub
parent 0bc5a3bc8d
commit 6059b5ef66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 3214 additions and 24689 deletions

View File

@ -1,5 +1,6 @@
import typing
from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle
from .variaRandomizer.utils.objectives import _goals
class StartItemsRemovesFromPool(Toggle):
"""Remove items in starting inventory from pool."""
@ -125,7 +126,7 @@ class AreaRandomization(Choice):
display_name = "Area Randomization"
option_off = 0
option_light = 1
option_on = 2
option_full = 2
default = 0
class AreaLayout(Toggle):
@ -183,9 +184,13 @@ class GravityBehaviour(Choice):
option_Progressive = 2
default = 1
class ElevatorsDoorsSpeed(DefaultOnToggle):
"""Accelerate doors and elevators transitions."""
display_name = "Elevators doors speed"
class ElevatorsSpeed(DefaultOnToggle):
"""Accelerate elevators transitions."""
display_name = "Elevators speed"
class DoorsSpeed(DefaultOnToggle):
"""Accelerate doors transitions."""
display_name = "Doors speed"
class SpinJumpRestart(Toggle):
"""Allows Samus to start spinning in mid air after jumping or falling."""
@ -239,6 +244,94 @@ class VariaCustomPreset(OptionList):
display_name = "Varia Custom Preset"
default = {}
class EscapeRando(Toggle):
"""
When leaving Tourian, get teleported to the exit of a random Map station (between Brinstar/Maridia/Norfair/Wrecked Ship).
You then have to find your way to the ship in the remaining time. Allotted time depends on area layout, but not on skill settings and is pretty generous.
During the escape sequence:
- All doors are opened
- Maridia tube is opened
- The Hyper Beam can destroy Bomb , Power Bomb and Super Missile blocks and open blue/green gates from both sides
- All mini bosses are defeated
- All minor enemies are removed to allow you to move faster and remove lag
During regular game only Crateria Map station door can be opened and activating the station will act as if all map stations were activated at once.
Animals Challenges:
You can use the extra available time to:
- find the animals that are hidden behind a (now blue) map station door
- go to the vanilla animals door to cycle through the 4 available escapes, and complete as many escapes as you can
Pick your challenge, or try to do both, but watch your timer!
"""
display_name = "Randomize the escape sequence"
class RemoveEscapeEnemies(Toggle):
"""Remove enemies during escape sequence, disable it to blast through enemies with your Hyper Beam and cause lag."""
display_name = "Remove enemies during escape"
class Tourian(Choice):
"""
Choose endgame Tourian behaviour:
Vanilla: regular vanilla Tourian
Fast: speed up Tourian to skip Metroids, Zebetites, and all cutscenes (including Mother Brain 3 fight). Golden Four statues are replaced by an invincible Gadora until all objectives are completed.
Disabled: skip Tourian entirely, ie. escape sequence is triggered as soon as all objectives are completed.
"""
display_name = "Endgame behavior with Tourian"
option_Vanilla = 0
option_Fast = 1
option_Disabled = 2
default = 0
class Objective(OptionList):
"""
Choose which objectives are required to sink the Golden Four statue and to open access to Tourian.
You can choose from 0 to 5 objectives.
Note: If you leave the list empty no objective is required to access Tourian, ie. it's open.
Note: See the Tourian parameter to enable fast Tourian or trigger the escape when all objectives are completed.
Note: Current percentage of collected items is displayed in the inventory pause menu.
Note: Collect 100% items is excluded by default when randomizing the objectives list as it requires you to complete all the objectives.
Note: In AP, Items% and areas objectives are counted toward location checks, not items collected or received, except for "collect all upgrades"
Format as a comma-separated list of objective names: ["kill three G4", "collect 75% items"].
A full list of supported objectives can be found at:
https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/sm/utils/objectives.py
"""
display_name = "Objectives"
default = ["kill all G4"]
valid_keys = frozenset({name: goal for (name, goal) in _goals.items()})
#valid_keys_casefold = True
class HideItems(Toggle):
"""
Hides half of the visible items.
Items always visible:
- Energy Tank, Gauntlet
- Energy Tank, Terminator
- Morphing Ball
- Missile (Crateria moat)
- Missile (green Brinstar below super missile)
- Missile (above Crocomire)
- Power Bomb (lower Norfair above fire flea room)
- Missile (Gravity Suit)
- Missile (green Maridia shinespark)
"""
display_name = "Hide half the items"
class RelaxedRoundRobinCF(Toggle):
"""
Changes Crystal Flashes behavior and requirements as follows:
You can perform a Crystal Flash with any amount of ammo, but you need at least one Power Bomb to begin the process.
After consuming 1 ammo, Samus gains 50 energy, and it will try a different ammo type next,
cycling through Missiles, Supers, and Power Bombs as available. The cycling is to keep the consumption even between ammo types.
If one of your ammo types is at 0, it will be skipped.
The Crystal Flash ends when Samus is out of ammo or a total of 30 ammo has been consumed.
"""
display_name = "Relaxed round robin Crystal Flash"
sm_options: typing.Dict[str, type(Option)] = {
"start_inventory_removes_from_pool": StartItemsRemovesFromPool,
"preset": Preset,
@ -254,7 +347,7 @@ sm_options: typing.Dict[str, type(Option)] = {
#"progression_difficulty": "normal",
"morph_placement": MorphPlacement,
#"suits_restriction": SuitsRestriction,
#"hide_items": "off",
"hide_items": HideItems,
"strict_minors": StrictMinors,
"missile_qty": MissileQty,
"super_qty": SuperQty,
@ -269,8 +362,8 @@ sm_options: typing.Dict[str, type(Option)] = {
#"minimizer": "off",
#"minimizer_qty": "45",
#"minimizer_tourian": "off",
#"escape_rando": "off",
#"remove_escape_enemies": "off",
"escape_rando": EscapeRando,
"remove_escape_enemies": RemoveEscapeEnemies,
"fun_combat": FunCombat,
"fun_movement": FunMovement,
"fun_suits": FunSuits,
@ -279,7 +372,8 @@ sm_options: typing.Dict[str, type(Option)] = {
"nerfed_charge": NerfedCharge,
"gravity_behaviour": GravityBehaviour,
#"item_sounds": "on",
"elevators_doors_speed": ElevatorsDoorsSpeed,
"elevators_speed": ElevatorsSpeed,
"fast_doors": DoorsSpeed,
"spin_jump_restart": SpinJumpRestart,
"rando_speed": SpeedKeep,
"infinite_space_jump": InfiniteSpaceJump,
@ -290,4 +384,7 @@ sm_options: typing.Dict[str, type(Option)] = {
"random_music": RandomMusic,
"custom_preset": CustomPreset,
"varia_custom_preset": VariaCustomPreset,
"tourian": Tourian,
"objective": Objective,
"relaxed_round_robin_cf": RelaxedRoundRobinCF,
}

View File

@ -86,7 +86,7 @@ class SMWorld(World):
game: str = "Super Metroid"
topology_present = True
data_version = 2
data_version = 3
option_definitions = sm_options
item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None}
@ -140,8 +140,8 @@ class SMWorld(World):
if (item in itemPool):
itemPool.remove(item)
missingPool = 105 - len(itemPool) + 1
for i in range(1, missingPool):
missingPool = 109 - len(itemPool)
for i in range(missingPool):
itemPool.append(ItemManager.Items['Nothing'])
# Generate item pool
@ -209,10 +209,11 @@ class SMWorld(World):
""" little-endian convert a 16-bit number to an array of numbers <= 255 each """
return [w & 0x00FF, (w & 0xFF00) >> 8]
# used for remote location Credits Spoiler of local items
# used for remote location Credits Spoiler of local items and Objectives' writeItemsMasks
class DummyLocation:
def __init__(self, name):
self.Name = name
self.restricted = False
def isBoss(self):
return False
@ -337,7 +338,7 @@ class SMWorld(World):
idx = 0
vanillaItemTypesCount = 21
for itemLoc in self.multiworld.get_locations():
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class:
# item to place in this SM world: write full item data to tables
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items:
itemId = ItemManager.Items[itemLoc.item.type].Id
@ -522,22 +523,38 @@ class SMWorld(World):
# commit all the changes we've made here to the ROM
romPatcher.commitIPS()
itemLocs = [
ItemLocation(ItemManager.Items[itemLoc.item.type
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else
'ArchipelagoItem'],
locationsDict[itemLoc.name], True)
for itemLoc in self.multiworld.get_locations() if itemLoc.player == self.player
]
romPatcher.writeItemsLocs(itemLocs)
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.multiworld.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.multiworld.get_locations() if itemLoc.item.player == self.player]
progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.multiworld.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.multiworld.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True]
# progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.multiworld.get_locations() if itemLoc.player == self.player and itemLoc.item.player == self.player and itemLoc.item.advancement == True]
romPatcher.writeObjectives(itemLocs, romPatcher.settings["tourian"])
romPatcher.writeItemsLocs(self.itemLocs)
# romPatcher.writeSplitLocs(self.variaRando.args.majorsSplit, itemLocs, progItemLocs)
romPatcher.writeItemsNumber()
if not romPatcher.settings["isPlando"]:
romPatcher.writeSeed(romPatcher.settings["seed"]) # lol if race mode
romPatcher.writeSpoiler(itemLocs, progItemLocs)
romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs)
romPatcher.writeDoorConnections(romPatcher.settings["doors"])
romPatcher.writeVersion(romPatcher.settings["displayedVersion"])
if romPatcher.settings["ctrlDict"] is not None:
romPatcher.writeControls(romPatcher.settings["ctrlDict"])
if romPatcher.settings["moonWalk"] == True:
romPatcher.enableMoonWalk()
romPatcher.writeMagic()
romPatcher.writeMajorsSplit(romPatcher.settings["majorsSplit"])
#if self.settings["isPlando"] and self.race is None:
# doorsPtrs = GraphUtils.getAps2DoorsPtrs()
# self.writePlandoTransitions(self.settings["plando"]["graphTrans"], doorsPtrs,
# self.settings["plando"]["maxTransitions"])
# self.writePlandoAddresses(self.settings["plando"]["visitedLocations"])
#if self.settings["isPlando"] and self.settings["plando"]["additionalETanks"] != 0:
# self.writeAdditionalETanks(self.settings["plando"]["additionalETanks"])
romPatcher.end()
def generate_output(self, output_directory: str):
outfilebase = self.multiworld.get_out_file_name_base(self.player)
@ -682,6 +699,41 @@ class SMWorld(World):
loc.place_locked_item(item)
loc.address = loc.item.code = None
def post_fill(self):
self.itemLocs = [
ItemLocation(ItemManager.Items[itemLoc.item.type
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else
'ArchipelagoItem'],
locationsDict[itemLoc.name], itemLoc.item.player, True)
for itemLoc in self.multiworld.get_locations(self.player)
]
self.progItemLocs = [
ItemLocation(ItemManager.Items[itemLoc.item.type
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else
'ArchipelagoItem'],
locationsDict[itemLoc.name], itemLoc.item.player, True)
for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement
]
for itemLoc in self.itemLocs:
if itemLoc.Item.Class == "Boss":
itemLoc.Item.Class = "Minor"
for itemLoc in self.progItemLocs:
if itemLoc.Item.Class == "Boss":
itemLoc.Item.Class = "Minor"
localItemLocs = [il for il in self.itemLocs if il.player == self.player]
localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player]
escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None
escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger)
assert escapeOk, "Could not find a solution for escape"
self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph,
self.variaRando.args.area, self.variaRando.args.bosses,
self.variaRando.args.escapeRando)
self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems)
@classmethod
def stage_post_fill(cls, world):
new_state = CollectionState(world)

View File

@ -12,8 +12,8 @@ B8:C831 :neg_1_7
B8:C843 :neg_1_8
B8:830C :pos_1_0
B8:8693 :pos_1_1
84:FA6B :pos_1_2
84:FA75 :pos_1_3
84:FE1B :pos_1_2
84:FE25 :pos_1_3
B8:C862 :pos_1_4
B8:C86F :pos_1_5
B8:C87C :pos_1_6
@ -27,18 +27,18 @@ B8:8503 COLLECTTANK
84:8BF2 NORMAL
85:FF4E SETFX
85:FF30 SOUNDFX
84:F9E0 SOUNDFX_84
84:FD90 SOUNDFX_84
85:FF3C SPECIALFX
84:F896 ammo_loop_table
84:FC46 ammo_loop_table
B8:85BA ap_playerid_to_rom_other_player_index
B8:85DD ap_playerid_to_rom_other_player_index_checklastrow
B8:85F8 ap_playerid_to_rom_other_player_index_correctindex
B8:85C0 ap_playerid_to_rom_other_player_index_do_search_stage_1
B8:85F5 ap_playerid_to_rom_other_player_index_notfound
84:F874 archipelago_chozo_item_plm
84:F878 archipelago_hidden_item_plm
84:F870 archipelago_visible_item_plm
84:F892 c_item
84:FC24 archipelago_chozo_item_plm
84:FC28 archipelago_hidden_item_plm
84:FC20 archipelago_visible_item_plm
84:FC42 c_item
CE:FF04 config_deathlink
CE:FF00 config_flags
CE:FF00 config_multiworld
@ -50,14 +50,14 @@ B8:83FD copy_memory
B8:8417 copy_memory_done
B8:8409 copy_memory_even
B8:840F copy_memory_loop
84:F894 h_item
84:F8AD i_chozo_item
84:F8B4 i_hidden_item
84:FA5A i_hidden_item_setup
84:FC44 h_item
84:FC5D i_chozo_item
84:FC64 i_hidden_item
84:FE0A i_hidden_item_setup
B8:885C i_item_setup_shared
B8:8878 i_item_setup_shared_all_items
B8:8883 i_item_setup_shared_alwaysloaded
84:FA79 i_live_pickup
84:FE29 i_live_pickup
B8:85FD i_live_pickup_multiworld
B8:8679 i_live_pickup_multiworld_end
B8:8659 i_live_pickup_multiworld_item_link_item
@ -65,18 +65,18 @@ B8:8649 i_live_pickup_multiworld_otherplayers_item
B8:8635 i_live_pickup_multiworld_own_item
B8:8641 i_live_pickup_multiworld_own_item1
B8:8620 i_live_pickup_multiworld_send_network
84:FA1E i_load_custom_graphics
84:FA39 i_load_custom_graphics_all_items
84:FA49 i_load_custom_graphics_alwaysloaded
84:FA61 i_load_rando_item
84:FA78 i_load_rando_item_end
84:F9F1 i_start_draw_loop
84:FA0A i_start_draw_loop_all_items
84:F9EC i_start_draw_loop_hidden
84:FA1C i_start_draw_loop_non_ammo_item
84:F9E5 i_start_draw_loop_visible_or_chozo
84:F8A6 i_visible_item
84:FA53 i_visible_item_setup
84:FDCE i_load_custom_graphics
84:FDE9 i_load_custom_graphics_all_items
84:FDF9 i_load_custom_graphics_alwaysloaded
84:FE11 i_load_rando_item
84:FE28 i_load_rando_item_end
84:FDA1 i_start_draw_loop
84:FDBA i_start_draw_loop_all_items
84:FD9C i_start_draw_loop_hidden
84:FDCC i_start_draw_loop_non_ammo_item
84:FD95 i_start_draw_loop_visible_or_chozo
84:FC56 i_visible_item
84:FE03 i_visible_item_setup
85:BB73 message_PlaceholderBig
85:BAF3 message_char_table
85:BBAA message_hook_tilemap_calc
@ -126,36 +126,36 @@ B8:84C4 mw_receive_item
B8:84FC mw_receive_item_end
B8:8469 mw_save_sram
B8:8442 mw_write_message
84:F888 nonprog_item_eight_palette_indices
84:FC38 nonprog_item_eight_palette_indices
89:9200 offworld_graphics_data_item
89:9100 offworld_graphics_data_progression_item
84:F972 p_chozo_item
84:F9A0 p_chozo_item_end
84:F98D p_chozo_item_loop
84:F999 p_chozo_item_trigger
84:F8FB p_etank_hloop
84:F8BB p_etank_loop
84:F9A6 p_hidden_item
84:F9D8 p_hidden_item_end
84:F9BD p_hidden_item_loop
84:F9A8 p_hidden_item_loop2
84:F9D1 p_hidden_item_trigger
84:F90F p_missile_hloop
84:F8CB p_missile_loop
84:F937 p_pb_hloop
84:F8EB p_pb_loop
84:F923 p_super_hloop
84:F8DB p_super_loop
84:F94B p_visible_item
84:F96E p_visible_item_end
84:F95B p_visible_item_loop
84:F967 p_visible_item_trigger
84:FD22 p_chozo_item
84:FD50 p_chozo_item_end
84:FD3D p_chozo_item_loop
84:FD49 p_chozo_item_trigger
84:FCAB p_etank_hloop
84:FC6B p_etank_loop
84:FD56 p_hidden_item
84:FD88 p_hidden_item_end
84:FD6D p_hidden_item_loop
84:FD58 p_hidden_item_loop2
84:FD81 p_hidden_item_trigger
84:FCBF p_missile_hloop
84:FC7B p_missile_loop
84:FCE7 p_pb_hloop
84:FC9B p_pb_loop
84:FCD3 p_super_hloop
84:FC8B p_super_loop
84:FCFB p_visible_item
84:FD1E p_visible_item_end
84:FD0B p_visible_item_loop
84:FD17 p_visible_item_trigger
B8:8694 patch_load_multiworld
84:FA7E perform_item_pickup
84:F886 plm_graphics_entry_offworld_item
84:F87C plm_graphics_entry_offworld_progression_item
84:FA90 plm_sequence_generic_item_0_bitmask
84:F87E prog_item_eight_palette_indices
84:FE2E perform_item_pickup
84:FC36 plm_graphics_entry_offworld_item
84:FC2C plm_graphics_entry_offworld_progression_item
84:FE40 plm_sequence_generic_item_0_bitmask
84:FC2E prog_item_eight_palette_indices
B8:E000 rando_item_table
B8:DCA0 rando_player_id_table
B8:DE34 rando_player_id_table_end
@ -168,20 +168,20 @@ B8:C800 start_item_data_major
B8:C808 start_item_data_minor
B8:C818 start_item_data_reserve
B8:C856 update_graphic
84:F890 v_item
84:FC40 v_item
B8:83EF write_repeated_memory
B8:83F4 write_repeated_memory_loop
[source files]
0000 8d746646 main.asm
0000 bf919aa9 main.asm
0001 06780555 ../common/nofanfare.asm
0002 ae952bc9 ../common/multiworld.asm
0003 613d24e1 ../common/itemextras.asm
0004 cc2c77e4 ../common/items.asm
0005 440b54fe ../common/startitem.asm
0003 f7e9db95 ../common/itemextras.asm
0004 b0dd378a ../common/items.asm
0005 dbfcb38d ../common/startitem.asm
[rom checksum]
f0ac0521
e8b10748
[addr-to-line mapping]
ff:ffff 0000:00000001
@ -767,101 +767,101 @@ b8:8884 0003:0000005a
b8:8885 0003:0000005b
b8:8886 0003:0000005c
b8:888a 0003:0000005d
84:f8a6 0004:00000051
84:f8a9 0004:00000052
84:f8ac 0004:00000053
84:f8ad 0004:00000056
84:f8b0 0004:00000057
84:f8b3 0004:00000058
84:f8b4 0004:0000005b
84:f8b7 0004:0000005c
84:f8ba 0004:0000005d
84:f9e0 0004:000000d4
84:f9e4 0004:000000d5
84:f9e5 0004:000000d8
84:f9e8 0004:000000d9
84:f9ea 0004:000000da
84:f9ec 0004:000000dd
84:f9ef 0004:000000de
84:f9f1 0004:000000e5
84:f9f2 0004:000000e6
84:f9f5 0004:000000e7
84:f9f8 0004:000000e7
84:f9f9 0004:000000e8
84:f9fd 0004:000000e9
84:fa00 0004:000000ea
84:fa02 0004:000000ee
84:fa05 0004:000000ef
84:fa06 0004:000000f0
84:fa0a 0004:000000f3
84:fa0d 0004:000000f4
84:fa0f 0004:000000f6
84:fa11 0004:000000f7
84:fa12 0004:000000f8
84:fa14 0004:000000f9
84:fa15 0004:000000fa
84:fa19 0004:000000fb
84:fa1a 0004:000000fc
84:fa1b 0004:000000fd
84:fa1c 0004:00000100
84:fa1d 0004:00000101
84:fa1e 0004:00000105
84:fa1f 0004:00000105
84:fa20 0004:00000105
84:fa21 0004:00000106
84:fa24 0004:00000107
84:fa27 0004:00000108
84:fa28 0004:00000109
84:fa2c 0004:0000010a
84:fa2f 0004:0000010b
84:fa31 0004:0000010d
84:fa34 0004:0000010e
84:fa35 0004:0000010e
84:fa39 0004:00000110
84:fa3a 0004:00000112
84:fa3b 0004:00000113
84:fa3c 0004:00000114
84:fa40 0004:00000115
84:fa42 0004:00000116
84:fa43 0004:00000117
84:fa44 0004:00000118
84:fa47 0004:00000119
84:fa48 0004:0000011a
84:fa49 0004:0000011d
84:fa4a 0004:0000011e
84:fa4c 0004:0000011f
84:fa4d 0004:00000120
84:fa51 0004:00000121
84:fa52 0004:00000122
84:fa53 0004:00000125
84:fa57 0004:00000126
84:fa5a 0004:00000129
84:fa5e 0004:0000012a
84:fa61 0004:0000012e
84:fa64 0004:0000012e
84:fa66 0004:0000012f
84:fa69 0004:00000130
84:fa6b 0004:00000131
84:fa6e 0004:00000131
84:fa70 0004:00000132
84:fa73 0004:00000133
84:fa75 0004:00000134
84:fa78 0004:00000137
84:fa79 0004:0000013b
84:fa7d 0004:0000013c
84:fa7e 0004:00000141
84:fa7f 0004:00000142
84:fa80 0004:00000143
84:fa81 0004:00000143
84:fa82 0004:00000147
84:fa86 0004:00000148
84:fa87 0004:00000149
84:fa88 0004:0000014a
84:fa89 0004:0000014a
84:fa8a 0004:0000014b
84:fa8d 0004:0000014c
84:fa8e 0004:0000014d
84:fa8f 0004:0000014e
84:fc56 0004:00000051
84:fc59 0004:00000052
84:fc5c 0004:00000053
84:fc5d 0004:00000056
84:fc60 0004:00000057
84:fc63 0004:00000058
84:fc64 0004:0000005b
84:fc67 0004:0000005c
84:fc6a 0004:0000005d
84:fd90 0004:000000d4
84:fd94 0004:000000d5
84:fd95 0004:000000d8
84:fd98 0004:000000d9
84:fd9a 0004:000000da
84:fd9c 0004:000000dd
84:fd9f 0004:000000de
84:fda1 0004:000000e5
84:fda2 0004:000000e6
84:fda5 0004:000000e7
84:fda8 0004:000000e7
84:fda9 0004:000000e8
84:fdad 0004:000000e9
84:fdb0 0004:000000ea
84:fdb2 0004:000000ee
84:fdb5 0004:000000ef
84:fdb6 0004:000000f0
84:fdba 0004:000000f3
84:fdbd 0004:000000f4
84:fdbf 0004:000000f6
84:fdc1 0004:000000f7
84:fdc2 0004:000000f8
84:fdc4 0004:000000f9
84:fdc5 0004:000000fa
84:fdc9 0004:000000fb
84:fdca 0004:000000fc
84:fdcb 0004:000000fd
84:fdcc 0004:00000100
84:fdcd 0004:00000101
84:fdce 0004:00000105
84:fdcf 0004:00000105
84:fdd0 0004:00000105
84:fdd1 0004:00000106
84:fdd4 0004:00000107
84:fdd7 0004:00000108
84:fdd8 0004:00000109
84:fddc 0004:0000010a
84:fddf 0004:0000010b
84:fde1 0004:0000010d
84:fde4 0004:0000010e
84:fde5 0004:0000010e
84:fde9 0004:00000110
84:fdea 0004:00000112
84:fdeb 0004:00000113
84:fdec 0004:00000114
84:fdf0 0004:00000115
84:fdf2 0004:00000116
84:fdf3 0004:00000117
84:fdf4 0004:00000118
84:fdf7 0004:00000119
84:fdf8 0004:0000011a
84:fdf9 0004:0000011d
84:fdfa 0004:0000011e
84:fdfc 0004:0000011f
84:fdfd 0004:00000120
84:fe01 0004:00000121
84:fe02 0004:00000122
84:fe03 0004:00000125
84:fe07 0004:00000126
84:fe0a 0004:00000129
84:fe0e 0004:0000012a
84:fe11 0004:0000012e
84:fe14 0004:0000012e
84:fe16 0004:0000012f
84:fe19 0004:00000130
84:fe1b 0004:00000131
84:fe1e 0004:00000131
84:fe20 0004:00000132
84:fe23 0004:00000133
84:fe25 0004:00000134
84:fe28 0004:00000137
84:fe29 0004:0000013b
84:fe2d 0004:0000013c
84:fe2e 0004:00000141
84:fe2f 0004:00000142
84:fe30 0004:00000143
84:fe31 0004:00000143
84:fe32 0004:00000147
84:fe36 0004:00000148
84:fe37 0004:00000149
84:fe38 0004:0000014a
84:fe39 0004:0000014a
84:fe3a 0004:0000014b
84:fe3d 0004:0000014c
84:fe3e 0004:0000014d
84:fe3f 0004:0000014e
81:b303 0005:00000003
81:b307 0005:00000004
81:b308 0005:00000005

View File

@ -9,18 +9,18 @@
"NORMAL": "84:8BF2",
"SETFX": "85:FF4E",
"SOUNDFX": "85:FF30",
"SOUNDFX_84": "84:F9E0",
"SOUNDFX_84": "84:FD90",
"SPECIALFX": "85:FF3C",
"ammo_loop_table": "84:F896",
"ammo_loop_table": "84:FC46",
"ap_playerid_to_rom_other_player_index": "B8:85BA",
"ap_playerid_to_rom_other_player_index_checklastrow": "B8:85DD",
"ap_playerid_to_rom_other_player_index_correctindex": "B8:85F8",
"ap_playerid_to_rom_other_player_index_do_search_stage_1": "B8:85C0",
"ap_playerid_to_rom_other_player_index_notfound": "B8:85F5",
"archipelago_chozo_item_plm": "84:F874",
"archipelago_hidden_item_plm": "84:F878",
"archipelago_visible_item_plm": "84:F870",
"c_item": "84:F892",
"archipelago_chozo_item_plm": "84:FC24",
"archipelago_hidden_item_plm": "84:FC28",
"archipelago_visible_item_plm": "84:FC20",
"c_item": "84:FC42",
"config_deathlink": "CE:FF04",
"config_flags": "CE:FF00",
"config_multiworld": "CE:FF00",
@ -32,14 +32,14 @@
"copy_memory_done": "B8:8417",
"copy_memory_even": "B8:8409",
"copy_memory_loop": "B8:840F",
"h_item": "84:F894",
"i_chozo_item": "84:F8AD",
"i_hidden_item": "84:F8B4",
"i_hidden_item_setup": "84:FA5A",
"h_item": "84:FC44",
"i_chozo_item": "84:FC5D",
"i_hidden_item": "84:FC64",
"i_hidden_item_setup": "84:FE0A",
"i_item_setup_shared": "B8:885C",
"i_item_setup_shared_all_items": "B8:8878",
"i_item_setup_shared_alwaysloaded": "B8:8883",
"i_live_pickup": "84:FA79",
"i_live_pickup": "84:FE29",
"i_live_pickup_multiworld": "B8:85FD",
"i_live_pickup_multiworld_end": "B8:8679",
"i_live_pickup_multiworld_item_link_item": "B8:8659",
@ -47,18 +47,18 @@
"i_live_pickup_multiworld_own_item": "B8:8635",
"i_live_pickup_multiworld_own_item1": "B8:8641",
"i_live_pickup_multiworld_send_network": "B8:8620",
"i_load_custom_graphics": "84:FA1E",
"i_load_custom_graphics_all_items": "84:FA39",
"i_load_custom_graphics_alwaysloaded": "84:FA49",
"i_load_rando_item": "84:FA61",
"i_load_rando_item_end": "84:FA78",
"i_start_draw_loop": "84:F9F1",
"i_start_draw_loop_all_items": "84:FA0A",
"i_start_draw_loop_hidden": "84:F9EC",
"i_start_draw_loop_non_ammo_item": "84:FA1C",
"i_start_draw_loop_visible_or_chozo": "84:F9E5",
"i_visible_item": "84:F8A6",
"i_visible_item_setup": "84:FA53",
"i_load_custom_graphics": "84:FDCE",
"i_load_custom_graphics_all_items": "84:FDE9",
"i_load_custom_graphics_alwaysloaded": "84:FDF9",
"i_load_rando_item": "84:FE11",
"i_load_rando_item_end": "84:FE28",
"i_start_draw_loop": "84:FDA1",
"i_start_draw_loop_all_items": "84:FDBA",
"i_start_draw_loop_hidden": "84:FD9C",
"i_start_draw_loop_non_ammo_item": "84:FDCC",
"i_start_draw_loop_visible_or_chozo": "84:FD95",
"i_visible_item": "84:FC56",
"i_visible_item_setup": "84:FE03",
"message_PlaceholderBig": "85:BB73",
"message_char_table": "85:BAF3",
"message_hook_tilemap_calc": "85:BBAA",
@ -108,36 +108,36 @@
"mw_receive_item_end": "B8:84FC",
"mw_save_sram": "B8:8469",
"mw_write_message": "B8:8442",
"nonprog_item_eight_palette_indices": "84:F888",
"nonprog_item_eight_palette_indices": "84:FC38",
"offworld_graphics_data_item": "89:9200",
"offworld_graphics_data_progression_item": "89:9100",
"p_chozo_item": "84:F972",
"p_chozo_item_end": "84:F9A0",
"p_chozo_item_loop": "84:F98D",
"p_chozo_item_trigger": "84:F999",
"p_etank_hloop": "84:F8FB",
"p_etank_loop": "84:F8BB",
"p_hidden_item": "84:F9A6",
"p_hidden_item_end": "84:F9D8",
"p_hidden_item_loop": "84:F9BD",
"p_hidden_item_loop2": "84:F9A8",
"p_hidden_item_trigger": "84:F9D1",
"p_missile_hloop": "84:F90F",
"p_missile_loop": "84:F8CB",
"p_pb_hloop": "84:F937",
"p_pb_loop": "84:F8EB",
"p_super_hloop": "84:F923",
"p_super_loop": "84:F8DB",
"p_visible_item": "84:F94B",
"p_visible_item_end": "84:F96E",
"p_visible_item_loop": "84:F95B",
"p_visible_item_trigger": "84:F967",
"p_chozo_item": "84:FD22",
"p_chozo_item_end": "84:FD50",
"p_chozo_item_loop": "84:FD3D",
"p_chozo_item_trigger": "84:FD49",
"p_etank_hloop": "84:FCAB",
"p_etank_loop": "84:FC6B",
"p_hidden_item": "84:FD56",
"p_hidden_item_end": "84:FD88",
"p_hidden_item_loop": "84:FD6D",
"p_hidden_item_loop2": "84:FD58",
"p_hidden_item_trigger": "84:FD81",
"p_missile_hloop": "84:FCBF",
"p_missile_loop": "84:FC7B",
"p_pb_hloop": "84:FCE7",
"p_pb_loop": "84:FC9B",
"p_super_hloop": "84:FCD3",
"p_super_loop": "84:FC8B",
"p_visible_item": "84:FCFB",
"p_visible_item_end": "84:FD1E",
"p_visible_item_loop": "84:FD0B",
"p_visible_item_trigger": "84:FD17",
"patch_load_multiworld": "B8:8694",
"perform_item_pickup": "84:FA7E",
"plm_graphics_entry_offworld_item": "84:F886",
"plm_graphics_entry_offworld_progression_item": "84:F87C",
"plm_sequence_generic_item_0_bitmask": "84:FA90",
"prog_item_eight_palette_indices": "84:F87E",
"perform_item_pickup": "84:FE2E",
"plm_graphics_entry_offworld_item": "84:FC36",
"plm_graphics_entry_offworld_progression_item": "84:FC2C",
"plm_sequence_generic_item_0_bitmask": "84:FE40",
"prog_item_eight_palette_indices": "84:FC2E",
"rando_item_table": "B8:E000",
"rando_player_id_table": "B8:DCA0",
"rando_player_id_table_end": "B8:DE34",
@ -150,7 +150,7 @@
"start_item_data_minor": "B8:C808",
"start_item_data_reserve": "B8:C818",
"update_graphic": "B8:C856",
"v_item": "84:F890",
"v_item": "84:FC40",
"write_repeated_memory": "B8:83EF",
"write_repeated_memory_loop": "B8:83F4",
"ITEM_RAM": "7E:09A2",

View File

@ -140,7 +140,7 @@ class AccessGraph(object):
def addAccessPoint(self, ap):
ap.distance = 0
self.accessPoints[ap.Name] = ap
self.accessPoints[ap.Name] = copy.deepcopy(ap)
def toDot(self, dotFile):
colors = ['red', 'blue', 'green', 'yellow', 'skyblue', 'violet', 'orange',
@ -175,6 +175,15 @@ class AccessGraph(object):
if both is True:
self.addTransition(dstName, srcName, False)
# remove transitions whose source or dest matches apName
def removeTransitions(self, apName):
toRemove = [t for t in self.InterAreaTransitions if t[0].Name == apName or t[1].Name == apName]
for t in toRemove:
src, dst = t
self.InterAreaTransitions.remove(t)
src.disconnect()
dst.disconnect()
# availNodes: all already available nodes
# nodesToCheck: nodes we have to check transitions for
# smbm: smbm to test logic on. if None, discard logic check, assume we can reach everything

View File

@ -238,7 +238,10 @@ class GraphUtils:
for ap in unusedAPs:
transitions.append((ap.Name, ap.Name))
def createMinimizerTransitions(startApName, locLimit):
# crateria can be forced in corner cases
def createMinimizerTransitions(startApName, locLimit, forcedAreas=None):
if forcedAreas is None:
forcedAreas = []
if startApName == 'Ceres':
startApName = 'Landing Site'
startAp = getAccessPoint(startApName)
@ -249,7 +252,10 @@ class GraphUtils:
return len([loc for loc in locList if locsPredicate(loc) == True and not loc.SolveArea.endswith(" Boss") and not loc.isBoss()])
availAreas = list(sorted({ap.GraphArea for ap in Logic.accessPoints if ap.GraphArea != startAp.GraphArea and getNLocs(lambda loc: loc.GraphArea == ap.GraphArea) > 0}))
areas = [startAp.GraphArea]
if startAp.GraphArea in forcedAreas:
forcedAreas.remove(startAp.GraphArea)
GraphUtils.log.debug("availAreas: {}".format(availAreas))
GraphUtils.log.debug("forcedAreas: {}".format(forcedAreas))
GraphUtils.log.debug("areas: {}".format(areas))
inBossCheck = lambda ap: ap.Boss and ap.Name.endswith("In")
nLocs = 0
@ -260,22 +266,27 @@ class GraphUtils:
def openTransitions():
nonlocal areas, inBossCheck, usedAPs
return GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and not ap.isInternal() and not inBossCheck(ap) and not ap in usedAPs)
while nLocs < locLimit or len(openTransitions()) < trLimit:
while nLocs < locLimit or len(openTransitions()) < trLimit or len(forcedAreas) > 0:
GraphUtils.log.debug("openTransitions="+str([ap.Name for ap in openTransitions()]))
fromAreas = availAreas
if nLocs >= locLimit:
if len(openTransitions()) <= 1: # dont' get stuck by adding dead ends
GraphUtils.log.debug("avoid being stuck")
fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1]
elif len(forcedAreas) > 0: # no risk to get stuck, we can pick a forced area if necessary
GraphUtils.log.debug("add forced area")
fromAreas = forcedAreas
elif nLocs >= locLimit: # we just need transitions, avoid adding a huge area
GraphUtils.log.debug("not enough open transitions")
# we just need transitions, avoid adding a huge area
fromAreas = []
n = trLimit - len(openTransitions())
while len(fromAreas) == 0:
fromAreas = [area for area in availAreas if len(GraphUtils.getAPs(lambda ap: not ap.isInternal())) > n]
n -= 1
minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas])
minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas])
fromAreas = [area for area in fromAreas if getNLocs(lambda loc: loc.GraphArea == area) == minLocs]
elif len(openTransitions()) <= 1: # dont' get stuck by adding dead ends
fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1]
nextArea = random.choice(fromAreas)
if nextArea in forcedAreas:
forcedAreas.remove(nextArea)
GraphUtils.log.debug("nextArea="+str(nextArea))
apCheck = lambda ap: not ap.isInternal() and not inBossCheck(ap) and ap not in usedAPs
possibleSources = GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and apCheck(ap))
@ -399,18 +410,37 @@ class GraphUtils:
def escapeAnimalsTransitions(graph, possibleTargets, firstEscape):
n = len(possibleTargets)
assert (n < 4 and firstEscape is not None) or (n <= 4 and firstEscape is None), "Invalid possibleTargets list: " + str(possibleTargets)
# first get our list of 4 entries for escape patch
GraphUtils.log.debug("escapeAnimalsTransitions. possibleTargets="+str(possibleTargets)+", firstEscape="+str(firstEscape))
if n >= 1:
# get actual animals: pick one of the remaining targets
animalsAccess = possibleTargets.pop()
graph.EscapeAttributes['Animals'] = animalsAccess
# we now have at most 3 targets left, fill up to fill cycling 4 targets for animals suprise
possibleTargets.append('Climb Bottom Left')
# complete possibleTargets. we need at least 2: one to
# hide the animals in, and one to connect the vanilla
# animals door to
if not any(t[1].Name == 'Climb Bottom Left' for t in graph.InterAreaTransitions):
# add standard Climb if not already in graph: it can be in Crateria-less minimizer + Disabled Tourian case
possibleTargets.append('Climb Bottom Left')
# make the escape possibilities loop by adding back the first escape
if firstEscape is not None:
possibleTargets.append(firstEscape)
poss = possibleTargets[:]
while len(possibleTargets) < 4:
possibleTargets.append(poss.pop(random.randint(0, len(poss)-1)))
n = len(possibleTargets)
# check if we can both hide the animals and connect the vanilla animals door to a cycling escape
if n >= 2:
# get actual animals: pick the first of the remaining targets (will contain a map room AP)
animalsAccess = possibleTargets.pop(0)
graph.EscapeAttributes['Animals'] = animalsAccess
# poss will contain the remaining map room AP(s) + optional AP(s) added above, to
# get the cycling 4 escapes from vanilla animals room
poss = possibleTargets[:]
GraphUtils.log.debug("escapeAnimalsTransitions. poss="+str(poss))
while len(possibleTargets) < 4:
if len(poss) > 1:
possibleTargets.append(poss.pop(random.randint(0, len(poss)-1)))
else:
# no more possible variety, spam the last possible escape
possibleTargets.append(poss[0])
else:
# failsafe: if not enough targets left, abort and do vanilla animals
animalsAccess = 'Flyway Right'

View File

@ -64,6 +64,8 @@ class Location:
smbm.removeItem(self.itemName)
self.difficulty = self.difficulty & postAvailable
if self.locDifficulty is not None:
self.locDifficulty = self.locDifficulty & postAvailable
def evalComeBack(self, smbm, areaGraph, ap):
if self.difficulty.bool == True:
@ -102,7 +104,7 @@ class Location:
def define_location(
Area, GraphArea, SolveArea, Name, Class, CanHidden, Address, Id,
Visibility, Room, VanillaItemType=None, AccessFrom=None, Available=None, PostAvailable=None, HUD=None):
Visibility, Room, VanillaItemType=None, BossItemType=None, AccessFrom=None, Available=None, PostAvailable=None, HUD=None):
name = Name.replace(' ', '').replace(',', '') + 'Location'
subclass = type(name, (Location,), {
'Area': Area,
@ -116,6 +118,7 @@ def define_location(
'Visibility': Visibility,
'Room': Room,
'VanillaItemType': VanillaItemType,
'BossItemType': BossItemType,
'HUD': HUD,
'AccessFrom': AccessFrom,
'Available': Available,
@ -322,6 +325,7 @@ define_location(
Id=None,
Visibility="Hidden",
Room='Kraid Room',
BossItemType="Kraid"
),
"Varia Suit":
define_location(
@ -445,12 +449,15 @@ define_location(
GraphArea="LowerNorfair",
SolveArea="Ridley Boss",
Name="Ridley",
Class=["Boss"],
Class=["Boss", "Scavenger"],
CanHidden=False,
Address=0xB055B056,
Id=None,
Id=0xaa,
Visibility="Hidden",
Room="Ridley's Room",
VanillaItemType="Ridley",
BossItemType="Ridley",
HUD=16
),
"Energy Tank, Ridley":
define_location(
@ -531,6 +538,7 @@ define_location(
Id=None,
Visibility="Hidden",
Room="Phantoon's Room",
BossItemType="Phantoon"
),
"Right Super, Wrecked Ship":
define_location(
@ -641,6 +649,7 @@ define_location(
Id=None,
Visibility="Hidden",
Room="Draygon's Room",
BossItemType="Draygon"
),
"Space Jump":
define_location(
@ -669,6 +678,63 @@ define_location(
Visibility="Hidden",
CanHidden=False,
Room='Mother Brain Room',
BossItemType="MotherBrain"
),
"Spore Spawn":
define_location(
Area="Brinstar",
GraphArea="GreenPinkBrinstar",
SolveArea="Pink Brinstar",
Name="Spore Spawn",
Class=["Boss"],
CanHidden=False,
Address=0xB055B055,
Id=None,
Visibility="Hidden",
Room='Spore Spawn Room',
BossItemType="SporeSpawn"
),
"Botwoon":
define_location(
Area="Maridia",
GraphArea="EastMaridia",
SolveArea="Maridia Pink Top",
Name="Botwoon",
Class=["Boss"],
CanHidden=False,
Address=0xB055B055,
Id=None,
Visibility="Hidden",
Room="Botwoon's Room",
BossItemType="Botwoon"
),
"Crocomire":
define_location(
Area="Norfair",
GraphArea="Crocomire",
SolveArea="Crocomire",
Name="Crocomire",
Class=["Boss"],
CanHidden=False,
Address=0xB055B055,
Id=None,
Visibility="Hidden",
Room="Crocomire's Room",
BossItemType="Crocomire"
),
"Golden Torizo":
define_location(
Area="LowerNorfair",
GraphArea="LowerNorfair",
SolveArea="Lower Norfair Screw Attack",
Name="Golden Torizo",
Class=["Boss"],
CanHidden=False,
Address=0xB055B055,
Id=None,
Visibility="Hidden",
Room="Golden Torizo's Room",
BossItemType="GoldenTorizo"
),
###### MINORS
"Power Bomb (Crateria surface)":

View File

@ -42,9 +42,8 @@ accessPoints = [
}, traverse = Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoMoreBlueDoors),
sm.traverse('GreenPiratesShaftBottomRight'))),
roomInfo = {'RoomPtr':0x99bd, "area": 0x0, 'songs':[0x99ce]},
# the doorAsmPtr 7FE00 is set by the g4_skip.ips patch, we have to call it
exitInfo = {'DoorPtr':0x8c52, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xfe00},
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xcc, 'SamusY':0x688, 'song': 0x9},
dotOrientation = 'e'),
AccessPoint('Moat Right', 'Crateria', {
@ -173,7 +172,9 @@ accessPoints = [
sm.canPassSpongeBath()),
sm.wand(sm.wnot(Bosses.bossDead(sm, 'Phantoon')),
RomPatches.has(sm.player, RomPatches.SpongeBathBlueDoor)))),
'PhantoonRoomOut': Cache.ldeco(lambda sm: sm.wand(sm.traverse('WreckedShipMainShaftBottom'), sm.canPassBombPassages()))
'PhantoonRoomOut': Cache.ldeco(lambda sm: sm.wand(sm.traverse('WreckedShipMainShaftBottom'), sm.canPassBombPassages())),
'Bowling': Cache.ldeco(lambda sm: sm.wand(sm.canMorphJump(),
sm.canPassBowling()))
}, internal=True,
start={'spawn':0x0300,
'doors':[0x83,0x8b], 'patches':[RomPatches.SpongeBathBlueDoor, RomPatches.WsEtankBlueDoor],
@ -183,6 +184,9 @@ accessPoints = [
'Wrecked Ship Main': lambda sm: SMBool(True),
'Crab Maze Left': Cache.ldeco(lambda sm: sm.canPassForgottenHighway(True))
}, internal=True),
AccessPoint('Bowling', 'WreckedShip', {
'West Ocean Left': lambda sm: SMBool(True)
}, internal=True),
AccessPoint('Crab Maze Left', 'WreckedShip', {
'Wrecked Ship Back': Cache.ldeco(lambda sm: sm.canPassForgottenHighway(False))
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoBlueDoors),
@ -245,6 +249,8 @@ accessPoints = [
sm.canUsePowerBombs()))
}, internal=True),
AccessPoint('LN Above GT', 'LowerNorfair', {
'LN Entrance': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canPassBombPassages())),
'Screw Attack Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.enoughStuffGT()))
}, internal=True),
@ -423,11 +429,9 @@ accessPoints = [
entryInfo = {'SamusX':0x134, 'SamusY':0x288, 'song': 0x15},
dotOrientation = 'se'),
AccessPoint('Crocomire Speedway Bottom', 'Norfair', {
'Business Center': Cache.ldeco(lambda sm: sm.wor(sm.wand(sm.canPassFrogSpeedwayRightToLeft(),
sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Norfair Entrance'])),
sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Norfair Entrance']),
sm.canGrappleEscape(),
sm.haveItem('Super')))),
'Grapple Escape': lambda sm: sm.canGrappleEscape(),
'Business Center': Cache.ldeco(lambda sm: sm.wand(sm.canPassFrogSpeedwayRightToLeft(),
sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Norfair Entrance']))),
'Bubble Mountain Bottom': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Bubble Mountain'])),
'Kronic Boost Room Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Kronic Boost Room <-> Croc']),
sm.haveItem('Morph')))
@ -437,6 +441,10 @@ accessPoints = [
"screen": (0x3, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xc57, 'SamusY':0x2b8},
dotOrientation = 'se'),
AccessPoint('Grapple Escape', 'Norfair', {
'Business Center': lambda sm: sm.haveItem('Super'),
'Crocomire Speedway Bottom': lambda sm: sm.canHellRunBackFromGrappleEscape()
}, internal=True),
AccessPoint('Bubble Mountain', 'Norfair', {
'Business Center': lambda sm: sm.canExitCathedral(Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Norfair Entrance']),
'Bubble Mountain Top': lambda sm: sm.canClimbBubbleMountain(),
@ -494,8 +502,7 @@ accessPoints = [
dotOrientation = 'se'),
### West Maridia
AccessPoint('Main Street Bottom', 'WestMaridia', {
'Red Fish Room Left': Cache.ldeco(lambda sm: sm.wand(sm.canGoUpMtEverest(),
sm.haveItem('Morph'))),
'Red Fish Room Bottom': lambda sm: sm.canGoUpMtEverest(),
'Crab Hole Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canTraverseCrabTunnelLeftToRight())),
# this transition leads to EastMaridia directly
@ -532,12 +539,17 @@ accessPoints = [
entryInfo = {'SamusX':0x28, 'SamusY':0x188},
dotOrientation = 'se'),
AccessPoint('Red Fish Room Left', 'WestMaridia', {
'Main Street Bottom': Cache.ldeco(lambda sm: sm.haveItem('Morph')) # just go down
'Red Fish Room Bottom': Cache.ldeco(lambda sm: sm.haveItem('Morph')) # just go down
}, roomInfo = {'RoomPtr':0xd104, "area": 0x4},
exitInfo = {'DoorPtr':0xa480, 'direction': 0x5, "cap": (0x2e, 0x36), "bitFlag": 0x40,
"screen": (0x2, 0x3), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe367},
entryInfo = {'SamusX':0x34, 'SamusY':0x88},
dotOrientation = 'w'),
AccessPoint('Red Fish Room Bottom', 'WestMaridia', {
'Main Street Bottom': lambda sm: SMBool(True), # just go down
'Red Fish Room Left': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canJumpUnderwater()))
}, internal=True),
AccessPoint('Crab Shaft Left', 'WestMaridia', {
'Main Street Bottom': lambda sm: SMBool(True), # fall down
'Beach': lambda sm: sm.canDoOuterMaridia(),
@ -586,7 +598,9 @@ accessPoints = [
dotOrientation = 'ne'),
### East Maridia
AccessPoint('Aqueduct Top Left', 'EastMaridia', {
'Aqueduct Bottom': lambda sm: sm.canUsePowerBombs()
'Aqueduct Bottom': lambda sm: sm.wor(sm.wand(RomPatches.has(sm.player, RomPatches.AqueductBombBlocks),
sm.canDestroyBombWallsUnderwater()),
sm.canUsePowerBombs())
}, roomInfo = {'RoomPtr':0xd5a7, "area": 0x4},
exitInfo = {'DoorPtr':0xa708, 'direction': 0x5, "cap": (0x1e, 0x36), "bitFlag": 0x0,
"screen": (0x1, 0x3), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe398},
@ -596,7 +610,8 @@ accessPoints = [
'Aqueduct Top Left': Cache.ldeco(lambda sm: sm.wand(sm.canDestroyBombWallsUnderwater(), # top left bomb blocks
sm.canJumpUnderwater())),
'Post Botwoon': Cache.ldeco(lambda sm: sm.wand(sm.canJumpUnderwater(),
sm.canDefeatBotwoon())), # includes botwoon hallway conditions
sm.canPassBotwoonHallway(),
sm.haveItem('Botwoon'))),
'Left Sandpit': lambda sm: sm.canAccessSandPits(),
'Right Sandpit': lambda sm: sm.canAccessSandPits(),
'Aqueduct': Cache.ldeco(lambda sm: sm.wand(sm.wor(sm.haveItem('SpeedBooster'),

View File

@ -241,6 +241,21 @@ class HelpersGraph(Helpers):
sm = self.smbm
return sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Speed Booster w/Speed' if sm.haveItem('SpeedBooster') else 'Bubble -> Speed Booster'])
# with door color rando, there can be situations where you have to come back from the missile
# loc without being able to open the speed booster door
@Cache.decorator
def canHellRunBackFromSpeedBoosterMissile(self):
sm = self.smbm
# require more health to count 1st hell run + way back is slower
hellrun = 'MainUpperNorfair'
tbl = Settings.hellRunsTable[hellrun]['Bubble -> Speed Booster']
mult = tbl['mult']
minE = tbl['minE']
mult *= 0.66 if sm.haveItem('SpeedBooster') else 0.33 # speed booster usable for 1st hell run
return sm.wor(RomPatches.has(sm.player, RomPatches.SpeedAreaBlueDoors),
sm.traverse('SpeedBoosterHallRight'),
sm.canHellRun(hellrun, mult, minE))
@Cache.decorator
def canAccessDoubleChamberItems(self):
sm = self.smbm
@ -253,6 +268,19 @@ class HelpersGraph(Helpers):
sm.knowsDoubleChamberWallJump()),
sm.canHellRun(hellRun['hellRun'], hellRun['mult']*0.8, hellRun['minE'])))
@Cache.decorator
def canExitWaveBeam(self):
sm = self.smbm
return sm.wor(sm.haveItem('Morph'), # exit through lower passage under the spikes
sm.wand(sm.wor(sm.haveItem('SpaceJump'), # exit through blue gate
sm.haveItem('Grapple')),
sm.wor(sm.haveItem('Wave'),
sm.wand(sm.heatProof(), # hell run + green gate glitch is too much
sm.canBlueGateGlitch(),
# if missiles were required to open the door, require two packs as no farming around
sm.wor(sm.wnot(SMBool('Missile' in sm.traverse('DoubleChamberRight').items)),
sm.itemCountOk("Missile", 2))))))
def canExitCathedral(self, hellRun):
# from top: can use bomb/powerbomb jumps
# from bottom: can do a shinespark or use space jump
@ -272,16 +300,40 @@ class HelpersGraph(Helpers):
@Cache.decorator
def canGrappleEscape(self):
sm = self.smbm
return sm.wor(sm.wor(sm.haveItem('SpaceJump'),
sm.wand(sm.canInfiniteBombJump(), # IBJ from lava...either have grav or freeze the enemy there if hellrunning (otherwise single DBJ at the end)
sm.wor(sm.heatProof(),
sm.haveItem('Gravity'),
sm.haveItem('Ice')))),
sm.haveItem('Grapple'),
sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.haveItem('HiJump'), # jump from the blocks below
sm.knowsShortCharge())), # spark from across the grapple blocks
sm.wand(sm.haveItem('HiJump'), sm.canSpringBallJump())) # jump from the blocks below
access = sm.wor(sm.wor(sm.haveItem('SpaceJump'),
sm.wand(sm.canInfiniteBombJump(), # IBJ from lava...either have grav or freeze the enemy there if hellrunning (otherwise single DBJ at the end)
sm.wor(sm.heatProof(),
sm.haveItem('Gravity'),
sm.haveItem('Ice')))),
sm.haveItem('Grapple'),
sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.haveItem('HiJump'), # jump from the blocks below
sm.knowsShortCharge())), # spark from across the grapple blocks
sm.wand(sm.haveItem('HiJump'), sm.canSpringBallJump())) # jump from the blocks below
hellrun = 'MainUpperNorfair'
tbl = Settings.hellRunsTable[hellrun]['Croc -> Norfair Entrance']
mult = tbl['mult']
minE = tbl['minE']
if 'InfiniteBombJump' in access.knows or 'ShortCharge' in access.knows:
mult *= 0.7
elif 'SpaceJump' in access.items:
mult *= 1.5
elif 'Grapple' in access.items:
mult *= 1.25
return sm.wand(access,
sm.canHellRun(hellrun, mult, minE))
@Cache.decorator
def canHellRunBackFromGrappleEscape(self):
sm = self.smbm
# require more health to count 1st hell run from croc speedway bottom to here+hellrun back (which is faster)
hellrun = 'MainUpperNorfair'
tbl = Settings.hellRunsTable[hellrun]['Croc -> Norfair Entrance']
mult = tbl['mult']
minE = tbl['minE']
mult *= 0.6
return sm.canHellRun(hellrun, mult, minE)
@Cache.decorator
def canPassFrogSpeedwayRightToLeft(self):
@ -733,7 +785,9 @@ class HelpersGraph(Helpers):
@Cache.decorator
def canExitPreciousRoomRandomized(self):
sm = self.smbm
suitlessRoomExit = sm.canSpringBallJump()
suitlessRoomExit = sm.wand(sm.wnot(sm.haveItem('Gravity')),
sm.canJumpUnderwater(),
sm.canSpringBallJump())
if suitlessRoomExit.bool == False:
if self.getDraygonConnection() == 'KraidRoomIn':
suitlessRoomExit = sm.canShortCharge() # charge spark in kraid's room
@ -764,3 +818,27 @@ class HelpersGraph(Helpers):
return sm.wand(sm.traverse('MainStreetBottomRight'),
sm.wor(sm.haveItem('Super'),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther)))
@Cache.decorator
def canAccessShaktoolFromPantsRoom(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Ice'), # puyo clip
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.knowsPuyoClip()),
sm.wand(sm.haveItem('Gravity'),
sm.haveItem('XRayScope'),
sm.knowsPuyoClipXRay()),
sm.knowsSuitlessPuyoClip())),
sm.wand(sm.haveItem('Grapple'), # go through grapple block
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.wor(sm.wand(sm.haveItem('HiJump'), sm.knowsAccessSpringBallWithHiJump()),
sm.haveItem('SpaceJump')),
sm.knowsAccessSpringBallWithGravJump(),
sm.wand(sm.haveItem('Bomb'),
sm.wor(sm.knowsAccessSpringBallWithBombJumps(),
sm.wand(sm.haveItem('SpringBall'),
sm.knowsAccessSpringBallWithSpringBallBombJumps()))),
sm.wand(sm.haveItem('SpringBall'), sm.knowsAccessSpringBallWithSpringBallJump()))),
sm.wand(sm.haveItem('SpaceJump'), sm.knowsAccessSpringBallWithFlatley()))),
sm.wand(sm.haveItem('XRayScope'), sm.knowsAccessSpringBallWithXRayClimb()), # XRay climb
sm.canCrystalFlashClip())

View File

@ -157,7 +157,7 @@ locationsDict["Energy Tank, Crocomire"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Crocomire"].Available = (
lambda sm: sm.wand(sm.enoughStuffCroc(),
lambda sm: sm.wand(sm.haveItem('Crocomire'),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.energyReserveCountOk(3/sm.getDmgReduction()[0])))
@ -176,7 +176,7 @@ locationsDict["Grapple Beam"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Grapple Beam"].Available = (
lambda sm: sm.wand(sm.enoughStuffCroc(),
lambda sm: sm.wand(sm.haveItem('Crocomire'),
sm.wor(sm.wand(sm.haveItem('Morph'),
sm.canFly()),
sm.wand(sm.haveItem('SpeedBooster'),
@ -220,11 +220,7 @@ locationsDict["Wave Beam"].Available = (
lambda sm: sm.traverse('DoubleChamberRight')
)
locationsDict["Wave Beam"].PostAvailable = (
lambda sm: sm.wor(sm.haveItem('Morph'), # exit through lower passage under the spikes
sm.wand(sm.wor(sm.haveItem('SpaceJump'), # exit through blue gate
sm.haveItem('Grapple')),
sm.wor(sm.wand(sm.canBlueGateGlitch(), sm.heatProof()), # hell run + green gate glitch is too much
sm.haveItem('Wave'))))
lambda sm: sm.canExitWaveBeam()
)
locationsDict["Ridley"].AccessFrom = {
'RidleyRoomIn': lambda sm: SMBool(True)
@ -233,7 +229,7 @@ locationsDict["Ridley"].Available = (
lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), sm.enoughStuffsRidley())
)
locationsDict["Energy Tank, Ridley"].AccessFrom = {
'RidleyRoomIn': lambda sm: sm.haveItem('Ridley')
'RidleyRoomIn': lambda sm: sm.wand(sm.haveItem('Ridley'), sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']))
}
locationsDict["Energy Tank, Ridley"].Available = (
lambda sm: sm.haveItem('Morph')
@ -354,27 +350,8 @@ locationsDict["Spring Ball"].AccessFrom = {
'Oasis Bottom': lambda sm: sm.canTraverseSandPits()
}
locationsDict["Spring Ball"].Available = (
lambda sm: sm.wand(sm.canUsePowerBombs(), # in Shaktool room to let Shaktool access the sand blocks
sm.wor(sm.wand(sm.haveItem('Ice'), # puyo clip
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.knowsPuyoClip()),
sm.wand(sm.haveItem('Gravity'),
sm.haveItem('XRayScope'),
sm.knowsPuyoClipXRay()),
sm.knowsSuitlessPuyoClip())),
sm.wand(sm.haveItem('Grapple'), # go through grapple block
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.wor(sm.wand(sm.haveItem('HiJump'), sm.knowsAccessSpringBallWithHiJump()),
sm.haveItem('SpaceJump')),
sm.knowsAccessSpringBallWithGravJump(),
sm.wand(sm.haveItem('Bomb'),
sm.wor(sm.knowsAccessSpringBallWithBombJumps(),
sm.wand(sm.haveItem('SpringBall'),
sm.knowsAccessSpringBallWithSpringBallBombJumps()))),
sm.wand(sm.haveItem('SpringBall'), sm.knowsAccessSpringBallWithSpringBallJump()))),
sm.wand(sm.haveItem('SpaceJump'), sm.knowsAccessSpringBallWithFlatley()))),
sm.wand(sm.haveItem('XRayScope'), sm.knowsAccessSpringBallWithXRayClimb()), # XRay climb
sm.canCrystalFlashClip()),
lambda sm: sm.wand(sm.canAccessShaktoolFromPantsRoom(),
sm.canUsePowerBombs(), # in Shaktool room to let Shaktool access the sand blocks
sm.wor(sm.haveItem('Gravity'), sm.canUseSpringBall())) # acess the item in spring ball room
)
locationsDict["Spring Ball"].PostAvailable = (
@ -406,10 +383,37 @@ locationsDict["Space Jump"].PostAvailable = (
lambda sm: Bosses.bossDead(sm, 'Draygon')
)
locationsDict["Mother Brain"].AccessFrom = {
'Golden Four': lambda sm: Bosses.allBossesDead(sm)
'Golden Four': lambda sm: sm.canPassG4()
}
locationsDict["Mother Brain"].Available = (
lambda sm: sm.enoughStuffTourian()
lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.NoTourian),
sm.enoughStuffTourian())
)
locationsDict["Spore Spawn"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Spore Spawn"].Available = (
lambda sm: sm.wand(sm.traverse('BigPinkTopRight'),
sm.enoughStuffSporeSpawn())
)
locationsDict["Botwoon"].AccessFrom = {
'Aqueduct Bottom': lambda sm: sm.canJumpUnderwater()
}
locationsDict["Botwoon"].Available = (
# includes botwoon hallway conditions
lambda sm: sm.canDefeatBotwoon()
)
locationsDict["Crocomire"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Crocomire"].Available = (
lambda sm: sm.enoughStuffCroc()
)
locationsDict["Golden Torizo"].AccessFrom = {
'Screw Attack Bottom': lambda sm: SMBool(True)
}
locationsDict["Golden Torizo"].Available = (
lambda sm: sm.enoughStuffGT()
)
locationsDict["Power Bomb (Crateria surface)"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
@ -506,10 +510,10 @@ locationsDict["Super Missile (pink Brinstar)"].AccessFrom = {
}
locationsDict["Super Missile (pink Brinstar)"].Available = (
lambda sm: sm.wor(sm.wand(sm.traverse('BigPinkTopRight'),
sm.enoughStuffSporeSpawn()),
sm.haveItem('SporeSpawn')),
# back way into spore spawn
sm.wand(sm.canOpenGreenDoors(),
sm.canPassBombPassages()))
sm.wand(sm.canOpenGreenDoors(),
sm.canPassBombPassages()))
)
locationsDict["Super Missile (pink Brinstar)"].PostAvailable = (
lambda sm: sm.wand(sm.canOpenGreenDoors(),
@ -665,10 +669,10 @@ locationsDict["Missile (below Ice Beam)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (above Crocomire)"].AccessFrom = {
'Crocomire Speedway Bottom': lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Grapple Escape Missiles'])
'Grapple Escape': lambda sm: SMBool(True)
}
locationsDict["Missile (above Crocomire)"].Available = (
lambda sm: sm.canGrappleEscape()
lambda sm: SMBool(True)
)
locationsDict["Missile (Hi-Jump Boots)"].AccessFrom = {
'Business Center': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.HiJumpAreaBlueDoor), sm.traverse('BusinessCenterBottomLeft'))
@ -691,7 +695,7 @@ locationsDict["Power Bomb (Crocomire)"].AccessFrom = {
}
locationsDict["Power Bomb (Crocomire)"].Available = (
lambda sm: sm.wand(sm.traverse('PostCrocomireUpperLeft'),
sm.enoughStuffCroc(),
sm.haveItem('Crocomire'),
sm.wor(sm.wor(sm.canFly(),
sm.haveItem('Grapple'),
sm.wand(sm.haveItem('SpeedBooster'),
@ -706,13 +710,13 @@ locationsDict["Missile (below Crocomire)"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Missile (below Crocomire)"].Available = (
lambda sm: sm.wand(sm.traverse('PostCrocomireShaftRight'), sm.enoughStuffCroc(), sm.haveItem('Morph'))
lambda sm: sm.wand(sm.traverse('PostCrocomireShaftRight'), sm.haveItem('Crocomire'), sm.haveItem('Morph'))
)
locationsDict["Missile (Grapple Beam)"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Missile (Grapple Beam)"].Available = (
lambda sm: sm.wand(sm.enoughStuffCroc(),
lambda sm: sm.wand(sm.haveItem('Crocomire'),
sm.wor(sm.wor(sm.wand(sm.haveItem('Morph'), # from below
sm.canFly()),
sm.wand(sm.haveItem('SpeedBooster'),
@ -754,6 +758,9 @@ locationsDict["Missile (Speed Booster)"].AccessFrom = {
locationsDict["Missile (Speed Booster)"].Available = (
lambda sm: sm.canHellRunToSpeedBooster()
)
locationsDict["Missile (Speed Booster)"].PostAvailable = (
lambda sm: sm.canHellRunBackFromSpeedBoosterMissile()
)
locationsDict["Missile (Wave Beam)"].AccessFrom = {
'Bubble Mountain Top': lambda sm: sm.canAccessDoubleChamberItems()
}
@ -773,7 +780,7 @@ locationsDict["Super Missile (Gold Torizo)"].AccessFrom = {
'Screw Attack Bottom': lambda sm: SMBool(True)
}
locationsDict["Super Missile (Gold Torizo)"].Available = (
lambda sm: SMBool(True)
lambda sm: sm.canDestroyBombWalls()
)
locationsDict["Super Missile (Gold Torizo)"].PostAvailable = (
lambda sm: sm.enoughStuffGT()

View File

@ -281,14 +281,23 @@ class Helpers(object):
def canMorphJump(self):
# small hop in morph ball form
sm = self.smbm
return sm.wor(sm.canPassBombPassages(), sm.haveItem('SpringBall'))
return sm.wor(sm.canPassBombPassages(), sm.canUseSpringBall())
def canCrystalFlash(self, n=1):
sm = self.smbm
return sm.wand(sm.canUsePowerBombs(),
sm.itemCountOk('Missile', 2*n),
sm.itemCountOk('Super', 2*n),
sm.itemCountOk('PowerBomb', 2*n+1))
if not RomPatches.has(sm.player, RomPatches.RoundRobinCF).bool:
ret = sm.wand(sm.canUsePowerBombs(),
sm.itemCountOk('Missile', 2*n),
sm.itemCountOk('Super', 2*n),
sm.itemCountOk('PowerBomb', 2*n+1))
else:
# simplified view of actual patch behavior: only count full refills to stick with base CF logic
nAmmo = 5 * (sm.itemCount('Missile') + sm.itemCount('Super') + sm.itemCount('PowerBomb'))
ret = sm.wand(sm.canUsePowerBombs(),
SMBool(nAmmo >= 30*n))
if ret:
ret._items.append("{}-CrystalFlash".format(n))
return ret
@Cache.decorator
def canCrystalFlashClip(self):
@ -363,7 +372,7 @@ class Helpers(object):
# - estimation of the fight duration in seconds (well not really, it
# is if you fire and land shots perfectly and constantly), giving info
# to compute boss fight difficulty
def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False):
def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False, missilesOffset=0, supersOffset=0, powerBombsOffset=0):
# TODO: handle special beam attacks ? (http://deanyd.net/sm/index.php?title=Charge_Beam_Combos)
sm = self.smbm
items = []
@ -379,14 +388,14 @@ class Helpers(object):
chargeDamage *= 3.0
# missile 100 damages, super missile 300 damages, PBs 200 dmg, 5 in each extension
missilesAmount = sm.itemCount('Missile') * 5
missilesAmount = max(0, (sm.itemCount('Missile') - missilesOffset)) * 5
if ignoreMissiles == True:
missilesDamage = 0
else:
missilesDamage = missilesAmount * 100
if missilesAmount > 0:
items.append('Missile')
supersAmount = sm.itemCount('Super') * 5
supersAmount = max(0, (sm.itemCount('Super') - supersOffset)) * 5
if ignoreSupers == True:
oneSuper = 0
else:
@ -399,7 +408,7 @@ class Helpers(object):
powerDamage = 0
powerAmount = 0
if power == True and sm.haveItem('PowerBomb') == True:
powerAmount = sm.itemCount('PowerBomb') * 5
powerAmount = max(0, (sm.itemCount('PowerBomb') - powerBombsOffset)) * 5
powerDamage = powerAmount * 200
items.append('PowerBomb')
@ -607,7 +616,9 @@ class Helpers(object):
# some ammo to destroy the turrets during the fight
if not sm.haveMissileOrSuper():
return smboolFalse
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000)
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000,
# underestimate missiles/supers in case a CF exit is needed
missilesOffset=2, supersOffset=2)
# print('DRAY', ammoMargin, secs)
if ammoMargin > 0:
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
@ -749,16 +760,20 @@ class Helpers(object):
class Pickup:
def __init__(self, itemsPickup):
self.itemsPickup = itemsPickup
self.endGameLocations = ["Mother Brain", "Gunship"]
# don't count end game location in the mix as it's now in the available locations
def enoughMinors(self, smbm, minorLocations):
if self.itemsPickup == 'all':
return len(minorLocations) == 0
if self.itemsPickup in ('all', 'all_strict'):
return (len(minorLocations) == 0
or (len(minorLocations) == 1 and minorLocations[0].Name in self.endGameLocations))
else:
return True
def enoughMajors(self, smbm, majorLocations):
if self.itemsPickup == 'all':
return len(majorLocations) == 0
if self.itemsPickup in ('all', 'all_strict'):
return (len(majorLocations) == 0
or (len(majorLocations) == 1 and majorLocations[0].Name in self.endGameLocations))
else:
return True
@ -811,9 +826,24 @@ class Bosses:
'WreckedShip Top': 'Phantoon'
}
accessPoints = {
"Kraid": "KraidRoomIn",
"Phantoon": "PhantoonRoomIn",
"Draygon": "Draygon Room Bottom",
"Ridley": "RidleyRoomIn",
"SporeSpawn": "Big Pink",
"Crocomire": "Crocomire Room Top",
"Botwoon": "Aqueduct Bottom",
"GoldenTorizo": "Screw Attack Bottom"
}
@staticmethod
def Golden4():
return ['Draygon', 'Kraid', 'Phantoon', 'Ridley']
@staticmethod
def miniBosses():
return ['SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo']
@staticmethod
def bossDead(sm, boss):
@ -831,6 +861,23 @@ class Bosses:
Bosses.bossDead(smbm, 'Phantoon'),
Bosses.bossDead(smbm, 'Draygon'),
Bosses.bossDead(smbm, 'Ridley'))
@staticmethod
def allMiniBossesDead(smbm):
return smbm.wand(Bosses.bossDead(smbm, 'SporeSpawn'),
Bosses.bossDead(smbm, 'Botwoon'),
Bosses.bossDead(smbm, 'Crocomire'),
Bosses.bossDead(smbm, 'GoldenTorizo'))
@staticmethod
def xBossesDead(smbm, target):
count = sum([1 for boss in Bosses.Golden4() if Bosses.bossDead(smbm, boss)])
return SMBool(count >= target)
@staticmethod
def xMiniBossesDead(smbm, target):
count = sum([1 for miniboss in Bosses.miniBosses() if Bosses.bossDead(smbm, miniboss)])
return SMBool(count >= target)
def diffValue2txt(diff):
last = 0

View File

@ -5,14 +5,16 @@ from ..logic.smbool import SMBool, smboolFalse
from ..logic.helpers import Bosses
from ..logic.logic import Logic
from ..utils.doorsmanager import DoorsManager
from ..utils.objectives import Objectives
from ..utils.parameters import Knows, isKnows
import logging
import sys
class SMBoolManager(object):
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4()
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper', 'Gunship'] + Bosses.Golden4() + Bosses.miniBosses()
countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve']
percentItems = ['Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack']
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False, lastAP = 'Landing Site'):
self._items = { }
self._counts = { }
@ -24,12 +26,13 @@ class SMBoolManager(object):
self.lastAP = lastAP
# cache related
self.cacheKey = 0
self.computeItemsPositions()
#self.cacheKey = 0
#self.computeItemsPositions()
Cache.reset()
Logic.factory('vanilla')
self.helpers = Logic.HelpersGraph(self)
self.doorsManager = DoorsManager()
self.objectives = Objectives.objDict[player]
self.createFacadeFunctions()
self.createKnowsFunctions(player)
self.resetItems()
@ -97,8 +100,8 @@ class SMBoolManager(object):
self._items = { item : smboolFalse for item in self.items }
self._counts = { item : 0 for item in self.countItems }
self.cacheKey = 0
Cache.update(self.cacheKey)
#self.cacheKey = 0
#Cache.update(self.cacheKey)
def addItem(self, item):
# a new item is available
@ -106,11 +109,11 @@ class SMBoolManager(object):
if self.isCountItem(item):
count = self._counts[item] + 1
self._counts[item] = count
self.computeNewCacheKey(item, count)
else:
self.computeNewCacheKey(item, 1)
#self.computeNewCacheKey(item, count)
#else:
#self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
#Cache.update(self.cacheKey)
def addItems(self, items):
if len(items) == 0:
@ -120,11 +123,11 @@ class SMBoolManager(object):
if self.isCountItem(item):
count = self._counts[item] + 1
self._counts[item] = count
self.computeNewCacheKey(item, count)
else:
self.computeNewCacheKey(item, 1)
#self.computeNewCacheKey(item, count)
#else:
#self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
#Cache.update(self.cacheKey)
def removeItem(self, item):
# randomizer removed an item (or the item was added to test a post available)
@ -133,12 +136,12 @@ class SMBoolManager(object):
self._counts[item] = count
if count == 0:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, count)
#self.computeNewCacheKey(item, count)
else:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, 0)
#self.computeNewCacheKey(item, 0)
Cache.update(self.cacheKey)
#Cache.update(self.cacheKey)
def createFacadeFunctions(self):
for fun in dir(self.helpers):
@ -147,22 +150,51 @@ class SMBoolManager(object):
def traverse(self, doorName):
return self.doorsManager.traverse(self, doorName)
def canPassG4(self):
return self.objectives.canClearGoals(self, 'Golden Four')
def hasItemsPercent(self, percent, totalItemsCount=None):
if totalItemsCount is None:
totalItemsCount = self.objectives.getTotalItemsCount()
currentItemsCount = self.getCollectedItemsCount()
return SMBool(100*(currentItemsCount/totalItemsCount) >= percent)
def getCollectedItemsCount(self):
return (len([item for item in self._items if self.haveItem(item) and item in self.percentItems])
+ sum([self.itemCount(item) for item in self._items if self.isCountItem(item)]))
def createKnowsFunctions(self, player):
# for each knows we have a function knowsKnows (ex: knowsAlcatrazEscape()) which
# take no parameter
for knows in Knows.__dict__:
if isKnows(knows):
if player in Knows.knowsDict and knows in Knows.knowsDict[player].__dict__:
setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.knowsDict[player].__dict__[knows].bool,
Knows.knowsDict[player].__dict__[knows].difficulty,
knows=[knows]))
else:
# if knows not in preset, use default values
setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.__dict__[knows].bool,
Knows.__dict__[knows].difficulty,
knows=[knows]))
self._createKnowsFunction(knows, player)
def _setKnowsFunction(self, knows, k):
setattr(self, 'knows'+knows, lambda: SMBool(k.bool, k.difficulty,
knows=[knows]))
def _createKnowsFunction(self, knows, player):
if player in Knows.knowsDict and knows in Knows.knowsDict[player].__dict__:
self._setKnowsFunction(knows, Knows.knowsDict[player].__dict__[knows])
else:
self._setKnowsFunction(knows, Knows.__dict__[knows])
def changeKnows(self, knows, newVal):
if isKnows(knows):
self._setKnowsFunction(knows, newVal)
#Cache.reset()
else:
raise ValueError("Invalid knows "+str(knows))
def restoreKnows(self, knows):
if isKnows(knows):
self._createKnowsFunction(knows)
#Cache.reset()
else:
raise ValueError("Invalid knows "+str(knows))
def isCountItem(self, item):
return item in self.countItems
@ -174,6 +206,12 @@ class SMBoolManager(object):
def haveItem(self, item):
#return self.state.has(item, self.player)
return self._items[item]
def haveItems(self, items):
for item in items:
if not self.haveItem(item):
return smboolFalse
return SMBool(True)
wand = staticmethod(SMBool.wand)
wandmax = staticmethod(SMBool.wandmax)
@ -218,11 +256,11 @@ class SMBoolManagerPlando(SMBoolManager):
if isCount:
count = self._counts[item] + 1
self._counts[item] = count
self.computeNewCacheKey(item, count)
else:
self.computeNewCacheKey(item, 1)
#self.computeNewCacheKey(item, count)
#else:
#self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
#Cache.update(self.cacheKey)
def removeItem(self, item):
# randomizer removed an item (or the item was added to test a post available)
@ -231,14 +269,14 @@ class SMBoolManagerPlando(SMBoolManager):
self._counts[item] = count
if count == 0:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, count)
#self.computeNewCacheKey(item, count)
else:
dup = 'dup_'+item
if self._items.get(dup, None) is None:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, 0)
#self.computeNewCacheKey(item, 0)
else:
del self._items[dup]
self.computeNewCacheKey(item, 1)
#self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
#Cache.update(self.cacheKey)

Binary file not shown.

View File

@ -51,8 +51,29 @@ patches = {
"Enable_Backup_Saves": {
0xef20: [0x1]
},
'Escape_Scavenger' : {
0x10F5FC: [0x1]
'Escape_Trigger' : {
0x10F5FE: [0x1]
},
'Escape_Trigger_Disable' : {
0x10F5FE: [0x0]
},
# actually a bitmask:
# high bit is for sfx play on obj completion, low bit for trigger escape
# only in crateria (standard in rando, default in the patch) for nothing objectives.
# we want to play sfx on objective completion only with non-standard objectives
'Objectives_sfx' : {
0x10F5FF: [0x81]
},
# see above, used in plandos so trigger escape whatever the start loc is
# with nothing objective. With this, we'll play sfx even in plandos
# with standard objectives, but it'll prevent to handle these patches
# as anything else that just bytes.
'Escape_Trigger_Nothing_Objective_Anywhere' : {
0x10F5FF: [0x80]
},
# for development/quickmet: disable clear save files on 1st boot
"Disable_Clear_Save_Boot": {
0x7E39: [0x4c, 0x7c, 0xfe]
},
# vanilla data to restore setup asm for plandos
"Escape_Animals_Disable": {
@ -96,6 +117,9 @@ patches = {
"SpriteSomething_Disable_Spin_Attack": {
0xD93FE: [0x0, 0x0]
},
"Ship_Takeoff_Disable_Hide_Samus": {
0x112B13: [0x6B]
},
# custom load points for non standard start APs
"Save_G4": {
# load point entry
@ -314,6 +338,32 @@ patches = {
0x78A34: [0x48, 0xc8, 0x01, 0x16, 0x47, 0x8c],
0x109F37: [0x0]
},
# only set blinking in "zebes asleep" room state to avoid having
# the door blink when not needed
# (only needed for escape peek in Crateria-less minimizer with disabled Tourian)
'Blinking[Climb Bottom Left]': {
0x782FE: [0x48, 0xc8, 0x01, 0x86, 0x12, 0x8c],
0x108683: [0x0]
},
# Climb always in "zebes asleep" state, except during escape
# (for escape peek in Crateria-less minimizer with disabled Tourian)
'Climb_Asleep': {
# replace "zebes awake" event ID with an unused event
0x796CC: [0x7F],
# put "Statues Hall" tension music
0x796D6: [0x04]
},
# Indicator PLM IDs set to ffff because they're set dynamically
'Indicator[KihunterBottom]': {
0x78256: [0xff, 0xff, 0x06, 0x02, 0x0e, 0x00]
},
'Indicator[GreenHillZoneTopRight]': {
0x78746: [0xff, 0xff, 0x01, 0x26, 0x30, 0x00]
},
# cancels the gamestate change by new_game.asm
"Restore_Intro": {
0x16EDA: [0x1E]
}
}
additional_PLMs = {
@ -550,4 +600,101 @@ additional_PLMs = {
[0x48, 0xc8, 0x01, 0x16, 0x63, 0x8c]
]
},
# Indicator PLM IDs set to ffff because they're set dynamically
'Indicator[LandingSiteRight]': {
'room': 0x948c,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x00, 0x00]
]
},
'Indicator[KihunterRight]': {
'room': 0x95ff,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x0d, 0x00]
]
},
'Indicator[NoobBridgeRight]': {
'room': 0xa253,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x46, 0x33, 0x00]
]
},
'Indicator[MainShaftBottomRight]': {
'room': 0x9cb3,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x22, 0x00]
]
},
'Indicator[BigPinkBottomRight]': {
'room': 0x9e52,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x29, 0x00]
]
},
'Indicator[RedTowerElevatorLeft]': {
'room': 0xa2f7,
'plm_bytes_list': [
[0xff, 0xff, 0x2e, 0x06, 0x3c, 0x00]
]
},
'Indicator[WestOceanRight]': {
'room': 0xca08,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x0c, 0x00]
]
},
'Indicator[LeCoudeBottom]': {
'room': 0x94cc,
'plm_bytes_list': [
[0xff, 0xff, 0x06, 0x02, 0x0f, 0x00]
]
},
'Indicator[WreckedShipMainShaftBottom]': {
'room': 0xcc6f,
'plm_bytes_list': [
[0xff, 0xff, 0x26, 0x02, 0x84, 0x00]
]
},
'Indicator[CathedralEntranceRight]': {
'room': 0xa788,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x4a, 0x00]
]
},
'Indicator[CathedralRight]': {
'room': 0xafa3,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x49, 0x00]
]
},
'Indicator[RedKihunterShaftBottom]': {
'room': 0xb5d5,
'plm_bytes_list': [
[0xff, 0xff, 0x56, 0x02, 0x5e, 0x00]
]
},
'Indicator[WastelandLeft]': {
'room': 0xb62b,
'plm_bytes_list': [
[0xff, 0xff, 0x2e, 0x06, 0x5f, 0x00]
]
},
'Indicator[MainStreetBottomRight]': {
'room': 0xd08a,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x8d, 0x00]
]
},
'Indicator[CrabShaftRight]': {
'room': 0xd5a7,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x16, 0x8f, 0x00]
]
},
'Indicator[ColosseumBottomRight]': {
'room': 0xd78f,
'plm_bytes_list': [
[0xff, 0xff, 0x01, 0x06, 0x9a, 0x00]
]
}
}

View File

@ -1,9 +1,13 @@
import random, copy
from ..utils import log
from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets
from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets, graphAreas, getAccessPoint
from ..logic.logic import Logic
from ..graph.graph import AccessGraphRando as AccessGraph
from ..logic.smbool import SMBool
from ..utils.objectives import Objectives
from ..rando.ItemLocContainer import getItemLocStr
from collections import defaultdict
# creates graph and handles randomized escape
class GraphBuilder(object):
@ -16,12 +20,37 @@ class GraphBuilder(object):
self.log = log.get('GraphBuilder')
# builds everything but escape transitions
def createGraph(self):
def createGraph(self, maxDiff):
transitions = self.graphSettings.plandoRandoTransitions
if transitions is None:
transitions = []
if self.minimizerN is not None:
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN)
forcedAreas = set()
# if no Crateria and auto escape trigger, we connect door connected to G4 to climb instead (see below).
# This wouldn't work here, as Tourian is isolated in the resulting seed (see below again)
# (well we could do two different transitions on both sides of doors, but that would just be confusing)
# so we force crateria to be in the graph
if self.graphSettings.startAP == "Golden Four" and self.graphSettings.tourian == "Disabled":
forcedAreas.add('Crateria')
# force areas required by objectives
# 1st the 'clear area' ones
forcedAreas = forcedAreas.union({goal.area for goal in Objectives.objDict[self.graphSettings.player].activeGoals if goal.area is not None})
# for the rest, base ourselves on escapeAccessPoints :
# - if only "1 of n" pick an area, preferably one already forced
# - filter out G4 AP (always there)
for goal in Objectives.objDict[self.graphSettings.player].activeGoals:
if goal.area is None:
n, apNames = goal.escapeAccessPoints
aps = [getAccessPoint(apName) for apName in apNames]
if len(aps) >= n:
n -= len([ap for ap in aps if ap.Boss])
escAreas = {ap.GraphArea for ap in aps if not ap.Boss}
objForced = forcedAreas.intersection(escAreas)
escAreasList = sorted(list(escAreas))
while len(objForced) < n and len(escAreasList) > 0:
objForced.add(escAreasList.pop(random.randint(0, len(escAreasList)-1)))
forcedAreas = forcedAreas.union(objForced)
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas)))
else:
if not self.bossRando:
transitions += vanillaBossesTransitions
@ -31,26 +60,44 @@ class GraphBuilder(object):
transitions += vanillaTransitions
else:
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando)
return AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
ret = AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
Objectives.objDict[self.graphSettings.player].setGraph(ret, maxDiff)
return ret
def addForeignItems(self, container, itemLocs):
itemPoolCounts = {}
for item in container.itemPool:
if item.Code is not None:
itemPoolCounts[item.Type] = itemPoolCounts.get(item.Type, 0) + 1
itemLocsCounts = {}
for il in itemLocs:
if il.Item.Code is not None and il.player == container.sm.player:
itemLocsCounts[il.Item.Type] = itemLocsCounts.get(il.Item.Type, 0) + 1
for item, count in itemPoolCounts.items():
for n in range(max(0, count - itemLocsCounts.get(item, 0))):
container.sm.addItem(item)
# fills in escape transitions if escape rando is enabled
# scavEscape = None or (itemLocs, scavItemLocs) couple from filler
def escapeGraph(self, container, graph, maxDiff, scavEscape):
# escapeTrigger = None or (itemLocs, progItemlocs) couple from filler
def escapeGraph(self, container, graph, maxDiff, escapeTrigger):
if not self.escapeRando:
return True
emptyContainer = copy.copy(container)
emptyContainer.resetCollected(reassignItemLocs=True)
dst = None
if scavEscape is None:
if escapeTrigger is None:
possibleTargets, dst, path = self.getPossibleEscapeTargets(emptyContainer, graph, maxDiff)
# update graph with escape transition
graph.addTransition(escapeSource, dst)
paths = [path]
else:
possibleTargets, path = self.getScavengerEscape(emptyContainer, graph, maxDiff, scavEscape)
if path is None:
self.addForeignItems(emptyContainer, escapeTrigger[0])
possibleTargets, paths = self.escapeTrigger(emptyContainer, graph, maxDiff, escapeTrigger)
if paths is None:
return False
# get timer value
self.escapeTimer(graph, path, self.areaRando or scavEscape is not None)
self.escapeTimer(graph, paths, self.areaRando or escapeTrigger is not None)
self.log.debug("escapeGraph: ({}, {}) timer: {}".format(escapeSource, dst, graph.EscapeAttributes['Timer']))
# animals
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst)
@ -68,10 +115,10 @@ class GraphBuilder(object):
def getPossibleEscapeTargets(self, emptyContainer, graph, maxDiff):
sm = emptyContainer.sm
# setup smbm with item pool
# Ice not usable because of hyper beam
# remove energy to avoid hell runs
# (will add bosses as well)
# setup smbm with item pool:
# - Ice not usable because of hyper beam
# - remove energy to avoid hell runs
# - (will add bosses as well)
sm.addItems([item.Type for item in emptyContainer.itemPool if item.Type != 'Ice' and item.Category != 'Energy'])
sm.addItem('Hyper')
possibleTargets = self._getTargets(sm, graph, maxDiff)
@ -80,55 +127,167 @@ class GraphBuilder(object):
path = graph.accessPath(sm, dst, 'Landing Site', maxDiff)
return (possibleTargets, dst, path)
def getScavengerEscape(self, emptyContainer, graph, maxDiff, scavEscape):
sm = emptyContainer.sm
itemLocs, lastScavItemLoc = scavEscape[0], scavEscape[1][-1]
# collect all item/locations up until last scav
for il in itemLocs:
emptyContainer.collect(il)
if il == lastScavItemLoc:
break
def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
container = emptyContainer
sm = container.sm
allItemLocs,progItemLocs,split = escapeTrigger[0],escapeTrigger[1],escapeTrigger[2]
# check if crateria is connected, if not replace Tourian
# connection with Climb and add special escape patch to Climb
if not any(il.Location.GraphArea == "Crateria" for il in allItemLocs):
escapeAttr = graph.EscapeAttributes
if "patches" not in escapeAttr:
escapeAttr['patches'] = []
escapeAttr['patches'] += ['climb_disable_bomb_blocks.ips', "Climb_Asleep"]
src, _ = next(t for t in graph.InterAreaTransitions if t[1].Name == "Golden Four")
graph.removeTransitions("Golden Four")
graph.addTransition(src.Name, "Climb Bottom Left")
# disconnect the other side of G4
graph.addTransition("Golden Four", "Golden Four")
# remove vanilla escape transition
graph.addTransition('Tourian Escape Room 4 Top Right', 'Tourian Escape Room 4 Top Right')
# filter garbage itemLocs
ilCheck = lambda il: not il.Location.isBoss() and not il.Location.restricted and il.Item.Category != "Nothing"
# update item% objectives
accessibleItems = [il.Item for il in allItemLocs if ilCheck(il)]
majorUpgrades = [item.Type for item in accessibleItems if item.BeamBits != 0 or item.ItemBits != 0]
sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades)
if split == "Scavenger":
# update escape access for scav with last scav loc
lastScavItemLoc = progItemLocs[-1]
sm.objectives.updateScavengerEscapeAccess(lastScavItemLoc.Location.accessPoint)
sm.objectives.setScavengerHuntFunc(lambda sm, ap: sm.haveItem(lastScavItemLoc.Item.Type))
else:
# update "collect all items in areas" funcs
availLocsByArea=defaultdict(list)
for itemLoc in allItemLocs:
if ilCheck(itemLoc) and (split.startswith("Full") or itemLoc.Location.isClass(split)):
availLocsByArea[itemLoc.Location.GraphArea].append(itemLoc.Location.Name)
self.log.debug("escapeTrigger. availLocsByArea="+str(availLocsByArea))
sm.objectives.setAreaFuncs({area:lambda sm,ap:SMBool(len(container.getLocs(lambda loc: loc.Name in availLocsByArea[area]))==0) for area in availLocsByArea})
self.log.debug("escapeTrigger. collect locs until G4 access")
# collect all item/locations up until we can pass G4 (the escape triggers)
itemLocs = allItemLocs[:]
ap = "Landing Site" # dummy value it'll be overwritten at first collection
while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)):
il = itemLocs.pop(0)
if il.Location.restricted or il.Item.Type == "ArchipelagoItem":
continue
self.log.debug("collecting " + getItemLocStr(il))
container.collect(il)
ap = il.Location.accessPoint
# final update of item% obj
collectedLocsAccessPoints = {il.Location.accessPoint for il in container.itemLocations}
sm.objectives.updateItemPercentEscapeAccess(list(collectedLocsAccessPoints))
possibleTargets = self._getTargets(sm, graph, maxDiff)
path = graph.accessPath(sm, lastScavItemLoc.Location.accessPoint, 'Landing Site', maxDiff)
return (possibleTargets, path)
# try to escape from all the possible objectives APs
possiblePaths = []
for goal in Objectives.objDict[self.graphSettings.player].activeGoals:
n, possibleAccessPoints = goal.escapeAccessPoints
count = 0
for ap in possibleAccessPoints:
self.log.debug("escapeTrigger. testing AP " + ap)
path = graph.accessPath(sm, ap, 'Landing Site', maxDiff)
if path is not None:
self.log.debug("escapeTrigger. add path from "+ap)
possiblePaths.append(path)
count += 1
if count < n:
# there is a goal we cannot escape from
self.log.debug("escapeTrigger. goal %s: found %d/%d possible escapes, abort" % (goal.name, count, n))
return (None, None)
# try and get a path from all possible areas
self.log.debug("escapeTrigger. completing paths")
allAreas = {il.Location.GraphArea for il in allItemLocs if not il.Location.restricted and not il.Location.GraphArea in ["Tourian", "Ceres"]}
def getStartArea(path):
return path[0].GraphArea
def apCheck(ap):
nonlocal graph, possiblePaths
apObj = graph.accessPoints[ap]
return apObj.GraphArea not in [getStartArea(path) for path in possiblePaths]
escapeAPs = [ap for ap in collectedLocsAccessPoints if apCheck(ap)]
for ap in escapeAPs:
path = graph.accessPath(sm, ap, 'Landing Site', maxDiff)
if path is not None:
self.log.debug("escapeTrigger. add path from "+ap)
possiblePaths.append(path)
def areaPathCheck():
nonlocal allAreas, possiblePaths
startAreas = {getStartArea(path) for path in possiblePaths}
return len(allAreas - startAreas) == 0
while not areaPathCheck() and len(itemLocs) > 0:
il = itemLocs.pop(0)
if il.Location.restricted or il.Item.Type == "ArchipelagoItem":
continue
self.log.debug("collecting " + getItemLocStr(il))
container.collect(il)
ap = il.Location.accessPoint
if apCheck(ap):
path = graph.accessPath(sm, ap, 'Landing Site', maxDiff)
if path is not None:
self.log.debug("escapeTrigger. add path from "+ap)
possiblePaths.append(path)
return (possibleTargets, possiblePaths)
def _computeTimer(self, graph, path):
traversedAreas = list(set([ap.GraphArea for ap in path]))
self.log.debug("escapeTimer path: " + str([ap.Name for ap in path]))
self.log.debug("escapeTimer traversedAreas: " + str(traversedAreas))
# rough estimates of navigation within areas to reach "borders"
# (can obviously be completely off wrt to actual path, but on the generous side)
traversals = {
'Crateria':90,
'GreenPinkBrinstar':90,
'WreckedShip':120,
'LowerNorfair':135,
'WestMaridia':75,
'EastMaridia':100,
'RedBrinstar':75,
'Norfair': 120,
'Kraid': 40,
'Crocomire': 40,
# can't be on the path
'Tourian': 0,
}
t = 90 if self.areaRando else 0
for area in traversedAreas:
t += traversals[area]
t = max(t, 180)
return t
# path: as returned by AccessGraph.accessPath
def escapeTimer(self, graph, path, compute):
if compute == True:
if path[0].Name == 'Climb Bottom Left':
graph.EscapeAttributes['Timer'] = None
return
traversedAreas = list(set([ap.GraphArea for ap in path]))
self.log.debug("escapeTimer path: " + str([ap.Name for ap in path]))
self.log.debug("escapeTimer traversedAreas: " + str(traversedAreas))
# rough estimates of navigation within areas to reach "borders"
# (can obviously be completely off wrt to actual path, but on the generous side)
traversals = {
'Crateria':90,
'GreenPinkBrinstar':90,
'WreckedShip':120,
'LowerNorfair':135,
'WestMaridia':75,
'EastMaridia':100,
'RedBrinstar':75,
'Norfair': 120,
'Kraid': 40,
'Crocomire': 40,
# can't be on the path
'Tourian': 0,
}
t = 90 if self.areaRando else 0
for area in traversedAreas:
t += traversals[area]
t = max(t, 180)
def escapeTimer(self, graph, paths, compute):
if len(paths) == 1:
path = paths.pop()
if compute == True:
if path[0].Name == 'Climb Bottom Left':
graph.EscapeAttributes['Timer'] = None
return
t = self._computeTimer(graph, path)
else:
escapeTargetsTimer = {
'Climb Bottom Left': None, # vanilla
'Green Brinstar Main Shaft Top Left': 210, # brinstar
'Basement Left': 210, # wrecked ship
'Business Center Mid Left': 270, # norfair
'Crab Hole Bottom Right': 270 # maridia
}
t = escapeTargetsTimer[path[0].Name]
self.log.debug("escapeTimer. t="+str(t))
graph.EscapeAttributes['Timer'] = t
else:
escapeTargetsTimer = {
'Climb Bottom Left': None, # vanilla
'Green Brinstar Main Shaft Top Left': 210, # brinstar
'Basement Left': 210, # wrecked ship
'Business Center Mid Left': 270, # norfair
'Crab Hole Bottom Right': 270 # maridia
}
t = escapeTargetsTimer[path[0].Name]
self.log.debug("escapeTimer. t="+str(t))
graph.EscapeAttributes['Timer'] = t
assert compute
graph.EscapeAttributes['Timer'] = 0
timerValues = {}
graph.EscapeAttributes['TimerTable'] = timerValues
for path in paths:
area = path[0].GraphArea
prev = timerValues.get(area, 0)
t = max(prev, self._computeTimer(graph, path))
timerValues[area] = t
self.log.debug("escapeTimer. area=%s, t=%d" % (area, t))
for area in graphAreas[1:-1]: # no Ceres or Tourian
if area not in timerValues:
# area not in graph most probably, still write a 10 minute "ultra failsafe" value
timerValues[area] = 600

View File

@ -6,12 +6,13 @@ from ..logic.smboolmanager import SMBoolManager
from collections import Counter
class ItemLocation(object):
__slots__ = ( 'Item', 'Location', 'Accessible' )
__slots__ = ( 'Item', 'Location', 'Accessible', 'player' )
def __init__(self, Item=None, Location=None, accessible=True):
def __init__(self, Item=None, Location=None, player=0, accessible=True):
self.Item = Item
self.Location = Location
self.Accessible = accessible
self.player = player
def json(self):
return {'Item': self.Item.json(), 'Location': self.Location.json()}

View File

@ -40,7 +40,7 @@ class ItemManager:
'ETank': Item(
Category='Energy',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Energy Tank",
Type='ETank',
Id=0
@ -48,7 +48,7 @@ class ItemManager:
'Missile': Item(
Category='Ammo',
Class='Minor',
Code=0xf870,
Code=0xfc20,
Name="Missile",
Type='Missile',
Id=1
@ -56,7 +56,7 @@ class ItemManager:
'Super': Item(
Category='Ammo',
Class='Minor',
Code=0xf870,
Code=0xfc20,
Name="Super Missile",
Type='Super',
Id=2
@ -64,7 +64,7 @@ class ItemManager:
'PowerBomb': Item(
Category='Ammo',
Class='Minor',
Code=0xf870,
Code=0xfc20,
Name="Power Bomb",
Type='PowerBomb',
Id=3
@ -72,7 +72,7 @@ class ItemManager:
'Bomb': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Bomb",
Type='Bomb',
ItemBits=0x1000,
@ -81,7 +81,7 @@ class ItemManager:
'Charge': Item(
Category='Beam',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Charge Beam",
Type='Charge',
BeamBits=0x1000,
@ -90,7 +90,7 @@ class ItemManager:
'Ice': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Ice Beam",
Type='Ice',
BeamBits=0x2,
@ -99,7 +99,7 @@ class ItemManager:
'HiJump': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Hi-Jump Boots",
Type='HiJump',
ItemBits=0x100,
@ -108,7 +108,7 @@ class ItemManager:
'SpeedBooster': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Speed Booster",
Type='SpeedBooster',
ItemBits=0x2000,
@ -117,7 +117,7 @@ class ItemManager:
'Wave': Item(
Category='Beam',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Wave Beam",
Type='Wave',
BeamBits=0x1,
@ -126,7 +126,7 @@ class ItemManager:
'Spazer': Item(
Category='Beam',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Spazer",
Type='Spazer',
BeamBits=0x4,
@ -135,7 +135,7 @@ class ItemManager:
'SpringBall': Item(
Category='Misc',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Spring Ball",
Type='SpringBall',
ItemBits=0x2,
@ -144,7 +144,7 @@ class ItemManager:
'Varia': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Varia Suit",
Type='Varia',
ItemBits=0x1,
@ -153,7 +153,7 @@ class ItemManager:
'Plasma': Item(
Category='Beam',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Plasma Beam",
Type='Plasma',
BeamBits=0x8,
@ -162,7 +162,7 @@ class ItemManager:
'Grapple': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Grappling Beam",
Type='Grapple',
ItemBits=0x4000,
@ -171,7 +171,7 @@ class ItemManager:
'Morph': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Morph Ball",
Type='Morph',
ItemBits=0x4,
@ -180,7 +180,7 @@ class ItemManager:
'Reserve': Item(
Category='Energy',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Reserve Tank",
Type='Reserve',
Id=20
@ -188,7 +188,7 @@ class ItemManager:
'Gravity': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Gravity Suit",
Type='Gravity',
ItemBits=0x20,
@ -197,7 +197,7 @@ class ItemManager:
'XRayScope': Item(
Category='Misc',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="X-Ray Scope",
Type='XRayScope',
ItemBits=0x8000,
@ -206,7 +206,7 @@ class ItemManager:
'SpaceJump': Item(
Category='Progression',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Space Jump",
Type='SpaceJump',
ItemBits=0x200,
@ -215,7 +215,7 @@ class ItemManager:
'ScrewAttack': Item(
Category='Misc',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Screw Attack",
Type='ScrewAttack',
ItemBits= 0x8,
@ -247,7 +247,7 @@ class ItemManager:
Category='Boss',
Class='Boss',
Name="Phantoon",
Type='Phantoon'
Type='Phantoon',
),
'Draygon': Item(
Category='Boss',
@ -267,6 +267,30 @@ class ItemManager:
Name="Mother Brain",
Type='MotherBrain',
),
'SporeSpawn': Item(
Category='MiniBoss',
Class='Boss',
Name="Spore Spawn",
Type='SporeSpawn',
),
'Crocomire': Item(
Category='MiniBoss',
Class='Boss',
Name="Crocomire",
Type='Crocomire',
),
'Botwoon': Item(
Category='MiniBoss',
Class='Boss',
Name="Botwoon",
Type='Botwoon',
),
'GoldenTorizo': Item(
Category='MiniBoss',
Class='Boss',
Name="Golden Torizo",
Type='GoldenTorizo',
),
# used only during escape path check
'Hyper': Item(
Category='Beam',
@ -278,7 +302,7 @@ class ItemManager:
'ArchipelagoItem': Item(
Category='ArchipelagoItem',
Class='Major',
Code=0xf870,
Code=0xfc20,
Name="Generic",
Type='ArchipelagoItem',
Id=21
@ -311,11 +335,12 @@ class ItemManager:
itemCode = item.Code + modifier
return itemCode
def __init__(self, majorsSplit, qty, sm, nLocs, maxDiff):
def __init__(self, majorsSplit, qty, sm, nLocs, bossesItems, maxDiff):
self.qty = qty
self.sm = sm
self.majorsSplit = majorsSplit
self.nLocs = nLocs
self.bossesItems = bossesItems
self.maxDiff = maxDiff
self.majorClass = 'Chozo' if majorsSplit == 'Chozo' else 'Major'
self.itemPool = []
@ -324,7 +349,7 @@ class ItemManager:
self.itemPool = []
if addBosses == True:
# for the bosses
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
for boss in self.bossesItems:
self.addMinor(boss)
def getItemPool(self):
@ -372,13 +397,17 @@ class ItemManager:
return len([item for item in self.itemPool if item.Type == itemName]) >= count
class ItemPoolGenerator(object):
# 100 item locs, 5 bosses, 4 mini bosses
maxLocs = 109
nbBosses = 9
@staticmethod
def factory(majorsSplit, itemManager, qty, sm, exclude, nLocs, maxDiff):
if majorsSplit == 'Chozo':
return ItemPoolGeneratorChozo(itemManager, qty, sm, maxDiff)
elif majorsSplit == 'Plando':
return ItemPoolGeneratorPlando(itemManager, qty, sm, exclude, nLocs, maxDiff)
elif nLocs == 105:
elif nLocs == ItemPoolGenerator.maxLocs:
if majorsSplit == "Scavenger":
return ItemPoolGeneratorScavenger(itemManager, qty, sm, maxDiff)
else:
@ -390,7 +419,7 @@ class ItemPoolGenerator(object):
self.itemManager = itemManager
self.qty = qty
self.sm = sm
self.maxItems = 105 # 100 item locs and 5 bosses
self.maxItems = ItemPoolGenerator.maxLocs
self.maxEnergy = 18 # 14E, 4R
self.maxDiff = maxDiff
self.log = log.get('ItemPool')
@ -405,7 +434,7 @@ class ItemPoolGenerator(object):
pool = self.itemManager.getItemPool()
energy = [item for item in pool if item.Category == 'Energy']
if len(energy) == 0:
self.maxMinors = 0.66*(self.maxItems - 5) # 5 for bosses
self.maxMinors = 0.66*(self.maxItems - ItemPoolGenerator.nbBosses)
else:
# if energy has been placed, we can be as accurate as possible
self.maxMinors = self.maxItems - len(pool) + self.nbMinorsAlready
@ -675,7 +704,8 @@ class ItemPoolGeneratorMinimizer(ItemPoolGeneratorMajors):
else:
self.maxEnergy = 8 + int(float(nLocs - 55)/50.0 * 8)
self.log.debug("maxEnergy: "+str(self.maxEnergy))
maxItems = self.maxItems - 10 # remove bosses and minimal minore
# remove bosses and minimal minors
maxItems = self.maxItems - (self.nbMinorsAlready + len(self.itemManager.bossesItems))
self.maxEnergy = int(max(self.maxEnergy, maxItems - nMajors - self.minorLocations))
if self.maxEnergy > 18:
self.maxEnergy = 18
@ -707,7 +737,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator):
if item == 'total':
continue
itemClass = 'Major'
if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain', 'SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo']:
itemClass = 'Minor'
for i in range(count):
self.itemManager.addItem(item, itemClass)
@ -716,7 +746,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator):
self.log.debug("Plando: remain start: {}".format(remain))
if remain > 0:
# add missing bosses
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
for boss in self.itemManager.bossesItems:
if self.exclude['alreadyPlacedItems'][boss] == 0:
self.itemManager.addItem(boss, 'Minor')
self.exclude['alreadyPlacedItems'][boss] = 1

View File

@ -1,63 +0,0 @@
import utils.log, random
from logic.smboolmanager import SMBoolManager
from utils.parameters import infinity
class MiniSolver(object):
def __init__(self, startAP, areaGraph, restrictions):
self.startAP = startAP
self.areaGraph = areaGraph
self.restrictions = restrictions
self.settings = restrictions.settings
self.smbm = SMBoolManager()
self.log = utils.log.get('MiniSolver')
# if True, does not mean it is actually beatable, unless you're sure of it from another source of information
# if False, it is certain it is not beatable
def isBeatable(self, itemLocations, maxDiff=None):
if maxDiff is None:
maxDiff = self.settings.maxDiff
minDiff = self.settings.minDiff
locations = []
for il in itemLocations:
loc = il.Location
if loc.restricted:
continue
loc.itemName = il.Item.Type
loc.difficulty = None
locations.append(loc)
self.smbm.resetItems()
ap = self.startAP
onlyBossesLeft = -1
hasOneLocAboveMinDiff = False
while True:
if not locations:
return hasOneLocAboveMinDiff
# only two loops to collect all remaining locations in only bosses left mode
if onlyBossesLeft >= 0:
onlyBossesLeft += 1
if onlyBossesLeft > 2:
return False
self.areaGraph.getAvailableLocations(locations, self.smbm, maxDiff, ap)
post = [loc for loc in locations if loc.PostAvailable and loc.difficulty.bool == True]
for loc in post:
self.smbm.addItem(loc.itemName)
postAvailable = loc.PostAvailable(self.smbm)
self.smbm.removeItem(loc.itemName)
loc.difficulty = self.smbm.wand(loc.difficulty, postAvailable)
toCollect = [loc for loc in locations if loc.difficulty.bool == True and loc.difficulty.difficulty <= maxDiff]
if not toCollect:
# mini onlyBossesLeft
if maxDiff < infinity:
maxDiff = infinity
onlyBossesLeft = 0
continue
return False
if not hasOneLocAboveMinDiff:
hasOneLocAboveMinDiff = any(loc.difficulty.difficulty >= minDiff for loc in locations)
self.smbm.addItems([loc.itemName for loc in toCollect])
for loc in toCollect:
locations.remove(loc)
# if len(locations) > 0:
# ap = random.choice([loc.accessPoint for loc in locations])

View File

@ -31,7 +31,7 @@ class RandoExec(object):
vcr = VCR(self.seedName, 'rando') if self.vcr == True else None
self.errorMsg = ""
split = self.randoSettings.restrictions['MajorMinor']
graphBuilder = GraphBuilder(self.graphSettings)
self.graphBuilder = GraphBuilder(self.graphSettings)
container = None
i = 0
attempts = 500 if self.graphSettings.areaRando or self.graphSettings.doorsColorsRando or split == 'Scavenger' else 1
@ -44,23 +44,28 @@ class RandoExec(object):
self.restrictions = Restrictions(self.randoSettings)
if self.graphSettings.doorsColorsRando == True:
DoorsManager.randomize(self.graphSettings.allowGreyDoors, self.player)
self.areaGraph = graphBuilder.createGraph()
self.areaGraph = self.graphBuilder.createGraph(self.randoSettings.maxDiff)
services = RandoServices(self.areaGraph, self.restrictions)
setup = RandoSetup(self.graphSettings, Logic.locations, services, self.player)
self.setup = setup
container = setup.createItemLocContainer(endDate, vcr)
if container is None:
sys.stdout.write('*')
sys.stdout.flush()
i += 1
else:
self.errorMsg += '\n'.join(setup.errorMsgs)
self.errorMsg += '; '.join(setup.errorMsgs)
now = time.process_time()
if container is None:
if self.graphSettings.areaRando:
self.errorMsg += "Could not find an area layout with these settings"
else:
self.errorMsg += "Unable to process settings"
self.errorMsg += "Could not find an area layout with these settings; "
if self.graphSettings.doorsColorsRando:
self.errorMsg += "Could not find a door color combination with these settings; "
if split == "Scavenger":
self.errorMsg += "Scavenger seed generation timed out; "
if setup.errorMsgs:
self.errorMsg += '; '.join(setup.errorMsgs)
if self.errorMsg == "":
self.errorMsg += "Unable to process settings; "
self.areaGraph.printGraph()
return container

View File

@ -26,6 +26,13 @@ class RandoServices(object):
self.cache = cache
self.log = log.get('RandoServices')
@staticmethod
def printProgress(s):
sys.stdout.write(s)
# avoid flushing I/O on pythonanywhere, as they are very slow
if os.getenv("PYTHONANYWHERE_DOMAIN") is None:
sys.stdout.flush()
# collect an item/loc with logic in a container from a given AP
# return new AP
def collect(self, ap, container, itemLoc, pickup=True):
@ -36,8 +43,7 @@ class RandoServices(object):
self.currentLocations(ap, container)
container.collect(itemLoc, pickup=pickup)
self.log.debug("COLLECT "+itemLoc.Item.Type+" at "+itemLoc.Location.Name)
sys.stdout.write('.')
sys.stdout.flush()
RandoServices.printProgress('.')
return itemLoc.Location.accessPoint if pickup == True else ap
# gives all the possible theoretical locations for a given item

View File

@ -32,11 +32,11 @@ class RandoSettings(object):
def isPlandoRando(self):
return self.PlandoOptions is not None
def getItemManager(self, smbm, nLocs):
def getItemManager(self, smbm, nLocs, bossesItems):
if not self.isPlandoRando():
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, self.maxDiff)
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, bossesItems, self.maxDiff)
else:
return ItemManager('Plando', self.qty, smbm, nLocs, self.maxDiff)
return ItemManager('Plando', self.qty, smbm, nLocs, bossesItems, self.maxDiff)
def getExcludeItems(self, locations):
if not self.isPlandoRando():
@ -67,7 +67,11 @@ class RandoSettings(object):
# Holds settings and utiliy functions related to graph layout
class GraphSettings(object):
def __init__(self, startAP, areaRando, lightAreaRando, bossRando, escapeRando, minimizerN, dotFile, doorsColorsRando, allowGreyDoors, plandoRandoTransitions):
def __init__(self, player, startAP, areaRando, lightAreaRando,
bossRando, escapeRando, minimizerN, dotFile,
doorsColorsRando, allowGreyDoors, tourian,
plandoRandoTransitions):
self.player = player
self.startAP = startAP
self.areaRando = areaRando
self.lightAreaRando = lightAreaRando
@ -77,6 +81,7 @@ class GraphSettings(object):
self.dotFile = dotFile
self.doorsColorsRando = doorsColorsRando
self.allowGreyDoors = allowGreyDoors
self.tourian = tourian
self.plandoRandoTransitions = plandoRandoTransitions
def isMinimizer(self):
@ -122,10 +127,16 @@ class ProgSpeedParameters(object):
elif progSpeed == 'fastest':
return 0.33
return 0
# chozo/slowest can make seed generation fail often, not much
# of a gameplay difference between slow/slowest in Chozo anyway,
# so we merge slow and slowest for some params
def isSlow(self, progSpeed):
return progSpeed == "slow" or (progSpeed == "slowest" and self.restrictions.split == "Chozo")
def getItemLimit(self, progSpeed):
itemLimit = self.nLocs
if progSpeed == 'slow':
if self.isSlow(progSpeed):
itemLimit = int(self.nLocs*0.209) # 21 for 105
elif progSpeed == 'medium':
itemLimit = int(self.nLocs*0.095) # 9 for 105
@ -143,7 +154,7 @@ class ProgSpeedParameters(object):
def getLocLimit(self, progSpeed):
locLimit = -1
if progSpeed == 'slow':
if self.isSlow(progSpeed):
locLimit = 1
elif progSpeed == 'medium':
locLimit = 2
@ -158,12 +169,12 @@ class ProgSpeedParameters(object):
if self.restrictions.isLateDoors():
progTypes += ['Wave','Spazer','Plasma']
progTypes.append('Charge')
if progSpeed == 'slowest':
if progSpeed == 'slowest' and self.restrictions.split != "Chozo":
return progTypes
else:
progTypes.remove('HiJump')
progTypes.remove('Charge')
if progSpeed == 'slow':
if self.isSlow(progSpeed):
return progTypes
else:
progTypes.remove('Bomb')

View File

@ -9,7 +9,9 @@ from ..graph.graph_utils import getAccessPoint, GraphUtils
from ..rando.Filler import FrontFiller
from ..rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr
from ..rando.Restrictions import Restrictions
from ..utils.objectives import Objectives
from ..utils.parameters import infinity
from ..rom.rom_patches import RomPatches
# checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions
# the entry point is createItemLocContainer
@ -27,7 +29,9 @@ class RandoSetup(object):
self.allLocations = locations
self.locations = self.areaGraph.getAccessibleLocations(locations, self.startAP)
# print("nLocs Setup: "+str(len(self.locations)))
self.itemManager = self.settings.getItemManager(self.sm, len(self.locations))
# in minimizer we can have some missing boss locs
bossesItems = [loc.BossItemType for loc in self.locations if loc.isBoss()]
self.itemManager = self.settings.getItemManager(self.sm, len(self.locations), bossesItems)
self.forbiddenItems = []
self.restrictedLocs = []
self.lastRestricted = []
@ -67,7 +71,12 @@ class RandoSetup(object):
for loc in self.restrictedLocs:
self.log.debug("createItemLocContainer: loc is restricted: {}".format(loc.Name))
loc.restricted = True
# checkDoorBeams calls checkPool, so save error messages
errorMsgsBck = self.errorMsgs[:]
self.checkDoorBeams()
self.errorMsgs = errorMsgsBck
self.container = ItemLocContainer(self.sm, self.getItemPool(), self.locations)
if self.restrictions.isLateMorph():
self.restrictions.lateMorphInit(self.startAP, self.container, self.services)
@ -122,7 +131,9 @@ class RandoSetup(object):
self.log.debug("fillRestrictedLocations. locs="+getLocListStr(locs))
for loc in locs:
itemLocation = ItemLocation(None, loc)
if self.container.hasItemInPool(getPred('Nothing', loc)):
if loc.BossItemType is not None:
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred(loc.BossItemType, loc))
elif self.container.hasItemInPool(getPred('Nothing', loc)):
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('Nothing', loc))
elif self.container.hasItemInPool(getPred('NoEnergy', loc)):
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('NoEnergy', loc))
@ -168,10 +179,13 @@ class RandoSetup(object):
self.log.debug("checkDoorBeams. mandatoryBeams="+str(self.restrictions.mandatoryBeams))
def checkPool(self, forbidden=None):
self.errorMsgs = []
self.log.debug("checkPool. forbidden=" + str(forbidden) + ", self.forbiddenItems=" + str(self.forbiddenItems))
if not self.graphSettings.isMinimizer() and not self.settings.isPlandoRando() and len(self.allLocations) > len(self.locations):
# invalid graph with looped areas
self.log.debug("checkPool: not all areas are connected, but minimizer param is off / not a plando rando")
msg = "not all areas are connected, but minimizer param is off / not a plando rando"
self.log.debug("checkPool: {}".format(msg))
self.errorMsgs.append(msg)
return False
ret = True
if forbidden is not None:
@ -185,7 +199,9 @@ class RandoSetup(object):
container = ItemLocContainer(self.sm, pool, self.locations)
except AssertionError as e:
# invalid graph altogether
self.log.debug("checkPool: AssertionError when creating ItemLocContainer: {}".format(e))
msg = "AssertionError when creating ItemLocContainer: {}".format(e)
self.log.debug("checkPool: {}".format(msg))
self.errorMsgs.append(msg)
return False
# restrict item pool in chozo: game should be finishable with chozo items only
contPool = []
@ -210,25 +226,55 @@ class RandoSetup(object):
self.lastRestricted = [loc for loc in self.locations if loc not in totalAvailLocs]
self.log.debug("restricted=" + str([loc.Name for loc in self.lastRestricted]))
# check if all inter-area APs can reach each other
interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()]
for startAp in interAPs:
availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff)
for ap in interAPs:
if not ap in availAccessPoints:
self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name))
# check if objectives are compatible with accessible APs
startAP = self.areaGraph.accessPoints[self.startAP]
availAPs = [ap.Name for ap in self.areaGraph.getAvailableAccessPoints(startAP, self.sm, self.settings.maxDiff)]
self.log.debug("availAPs="+str(availAPs))
for goal in Objectives.objDict[self.graphSettings.player].activeGoals:
n, aps = goal.escapeAccessPoints
if len(aps) == 0:
continue
escAPs = [ap for ap in aps if ap in availAPs]
self.log.debug("escAPs="+str(escAPs))
if len(escAPs) < n:
msg = "goal '{}' impossible to complete due to area layout".format(goal.name)
self.log.debug("checkPool. {}".format(msg))
self.errorMsgs.append(msg)
ret = False
continue
for ap in escAPs:
if not self.areaGraph.canAccess(self.sm, ap, "Golden Four", self.settings.maxDiff):
msg = "goal '{}' impossible to complete due to area layout".format(goal.name)
self.log.debug("checkPool. {}".format(msg))
self.errorMsgs.append(msg)
ret = False
if not ret:
self.log.debug("checkPool. inter-area APs check failed")
break
# check if all inter-area APs can reach each other
if ret:
interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()]
for startAp in interAPs:
availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff)
for ap in interAPs:
if not ap in availAccessPoints:
self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name))
ret = False
if not ret:
msg = "inter-area APs check failed"
self.log.debug("checkPool. {}".format(msg))
self.errorMsgs.append(msg)
# cleanup
self.sm.resetItems()
self.restoreBossChecks()
# check if we can reach/beat all bosses
if ret:
# always add G4 to mandatory bosses, even if not required by objectives
mandatoryBosses = set(Objectives.objDict[self.sm.player].getMandatoryBosses() + Bosses.Golden4())
for loc in self.lastRestricted:
if loc.Name in self.bossesLocs:
ret = False
self.log.debug("unavail Boss: " + loc.Name)
msg = "unavail Boss: {}".format(loc.Name)
self.log.debug("checkPool. {}".format(msg))
if ret:
# revive bosses
self.sm.addItems([item.Type for item in contPool if item.Category != 'Boss'])
@ -238,17 +284,24 @@ class RandoSetup(object):
and self.areaGraph.canAccess(self.sm, self.startAP, 'DraygonRoomIn', maxDiff)
if ret:
# see if we can beat bosses with this equipment (infinity as max diff for a "onlyBossesLeft" type check
beatableBosses = sorted([loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()])
beatableBosses = sorted([loc.BossItemType for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()])
self.log.debug("checkPool. beatableBosses="+str(beatableBosses))
ret = beatableBosses == Bosses.Golden4()
self.log.debug("checkPool. mandatoryBosses: {}".format(mandatoryBosses))
ret = mandatoryBosses.issubset(set(beatableBosses)) and Objectives.objDict[self.sm.player].checkLimitObjectives(beatableBosses)
if ret:
# check that we can then kill mother brain
self.sm.addItems(Bosses.Golden4())
self.sm.addItems(Bosses.Golden4() + Bosses.miniBosses())
beatableMotherBrain = [loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.Name == 'Mother Brain']
ret = len(beatableMotherBrain) > 0
self.log.debug("checkPool. beatable Mother Brain={}".format(ret))
else:
msg = "can't kill all mandatory bosses/minibosses: {}".format(', '.join(list(mandatoryBosses - set(beatableBosses))))
self.log.debug("checkPool. {}".format(msg))
self.errorMsgs.append(msg)
else:
self.log.debug('checkPool. locked by Phantoon or Draygon')
msg = "locked by Phantoon or Draygon"
self.log.debug('checkPool. {}'.format(msg))
self.errorMsgs.append(msg)
self.log.debug('checkPool. boss access sanity check: '+str(ret))
if self.restrictions.isChozo() or self.restrictions.isScavenger():
@ -319,7 +372,6 @@ class RandoSetup(object):
else:
forb = []
self.forbiddenItems += forb
self.checkPool()
self.addRestricted()
return len(forb)
@ -344,6 +396,9 @@ class RandoSetup(object):
def getForbiddenMovement(self):
self.log.debug("getForbiddenMovement BEGIN. forbidden="+str(self.forbiddenItems))
removableMovement = [mvt for mvt in self.movementItems if self.checkPool([mvt])]
if 'Bomb' in removableMovement and not RomPatches.has(self.sm.player, RomPatches.BombTorizoWake) and Objectives.objDict[self.sm.player].isGoalActive("activate chozo robots"):
# in this objective, without VARIA tweaks, BT has to wake so give bombs
removableMovement.remove('Bomb')
self.log.debug("getForbiddenMovement removable="+str(removableMovement))
if len(removableMovement) > 0:
# remove at least the most important

View File

@ -14,11 +14,9 @@ class Restrictions(object):
self.suitsRestrictions = settings.restrictions['Suits']
self.scavLocs = None
self.scavIsVanilla = False
self.scavEscape = False
self.restrictionDictChecker = None
if self.split == 'Scavenger':
self.scavIsVanilla = settings.restrictions['ScavengerParams']['vanillaItems']
self.scavEscape = settings.restrictions['ScavengerParams']['escape']
# checker function chain used by canPlaceAtLocation
self.checkers = self.getCheckers()
self.static = {}
@ -84,7 +82,7 @@ class Restrictions(object):
self.checkers.append(self.restrictionDictChecker)
def isLocMajor(self, loc):
return not loc.isBoss() and (self.split == "Full" or loc.isClass(self.split))
return (not loc.isBoss() and self.split == "Full") or loc.isClass(self.split)
def isLocMinor(self, loc):
return not loc.isBoss() and (self.split == "Full" or not loc.isClass(self.split))
@ -93,7 +91,7 @@ class Restrictions(object):
if self.split == "Full":
return True
elif self.split == 'Scavenger':
return not self.isItemMinor(item)
return not self.isItemMinor(item) or item.Type == "Ridley"
else:
return item.Class == self.split
@ -135,7 +133,7 @@ class Restrictions(object):
def getCheckers(self):
checkers = []
self.log.debug("add bosses restriction")
checkers.append(lambda item, loc, cont: (item.Category != 'Boss' and not loc.isBoss()) or (item.Category == 'Boss' and item.Name == loc.Name))
checkers.append(lambda item, loc, cont: (item.Category not in ['Boss', 'MiniBoss'] and not loc.isBoss()) or (item.Category in ['Boss', 'MiniBoss'] and item.Type == loc.BossItemType))
if self.split != 'Full':
if self.split != 'Scavenger':
self.log.debug("add majorsSplit restriction")

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,8 @@ from .utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, g
from .utils.version import displayedVersion
from .utils.doorsmanager import DoorsManager
from .logic.logic import Logic
from .utils.objectives import Objectives
from .utils.utils import dumpErrorMsg
from .utils import log
from ..Options import StartLocation
@ -33,6 +35,9 @@ progDiffs = defaultMultiValues['progressionDifficulty']
morphPlacements = defaultMultiValues['morphPlacement']
majorsSplits = defaultMultiValues['majorsSplit']
gravityBehaviours = defaultMultiValues['gravityBehaviour']
objectives = defaultMultiValues['objective']
tourians = defaultMultiValues['tourian']
areaRandomizations = defaultMultiValues['areaRandomization']
def randomMulti(args, param, defaultMultiValues):
value = args[param]
@ -40,7 +45,7 @@ def randomMulti(args, param, defaultMultiValues):
isRandom = False
if value == "random":
isRandom = True
if args[param+"List"] != None:
if args[param+"List"] is not None:
# use provided list
choices = args[param+"List"].split(',')
value = random.choice(choices)
@ -50,12 +55,6 @@ def randomMulti(args, param, defaultMultiValues):
return (isRandom, value)
def dumpErrorMsg(outFileName, msg):
print("DIAG: " + msg)
if outFileName is not None:
with open(outFileName, 'w') as jsonFile:
json.dump({"errorMsg": msg}, jsonFile)
def dumpErrorMsgs(outFileName, msgs):
dumpErrorMsg(outFileName, joinErrorMsgs(msgs))
@ -74,9 +73,6 @@ def to_pascal_case_with_space(snake_str):
class VariaRandomizer:
parser = argparse.ArgumentParser(description="Random Metroid Randomizer")
parser.add_argument('--patchOnly',
help="only apply patches, do not perform any randomization", action='store_true',
dest='patchOnly', default=False)
parser.add_argument('--param', '-p', help="the input parameters",
default=None, dest='paramsFileName')
parser.add_argument('--dir',
@ -86,12 +82,12 @@ class VariaRandomizer:
help="generate dot file with area graph",
action='store_true',dest='dot', default=False)
parser.add_argument('--area', help="area mode",
dest='area', nargs='?', const=True, default=False)
dest='area', nargs='?', const=True, choices=["random"]+areaRandomizations, default='off')
parser.add_argument('--areaList', help="list to choose from when random",
dest='areaList', nargs='?', default=None)
parser.add_argument('--areaLayoutBase',
help="use simple layout patch for area mode", action='store_true',
dest='areaLayoutBase', default=False)
parser.add_argument('--lightArea', help="keep number of transitions between vanilla areas", action='store_true',
dest='lightArea', default=False)
parser.add_argument('--escapeRando',
help="Randomize the escape sequence",
dest='escapeRando', nargs='?', const=True, default=False)
@ -103,9 +99,6 @@ class VariaRandomizer:
parser.add_argument('--minimizer', help="minimizer mode: area and boss mixed together. arg is number of non boss locations",
dest='minimizerN', nargs='?', const=35, default=None,
choices=[str(i) for i in range(30,101)]+["random"])
parser.add_argument('--minimizerTourian',
help="Tourian speedup in minimizer mode",
dest='minimizerTourian', nargs='?', const=True, default=False)
parser.add_argument('--startLocation', help="Name of the Access Point to start from",
dest='startLocation', nargs='?', default="Landing Site",
choices=['random'] + GraphUtils.getStartAccessPointNames())
@ -135,11 +128,11 @@ class VariaRandomizer:
parser.add_argument('--patch', '-c',
help="optional patches to add",
dest='patches', nargs='?', default=[], action='append',
choices=['itemsounds.ips', 'elevators_doors_speed.ips', 'random_music.ips',
'spinjumprestart.ips', 'rando_speed.ips', 'No_Music', 'AimAnyButton.ips',
'max_ammo_display.ips', 'supermetroid_msu1.ips', 'Infinite_Space_Jump',
'refill_before_save.ips', 'remove_elevators_doors_speed.ips',
'remove_itemsounds.ips', 'vanilla_music.ips'])
choices=['itemsounds.ips', 'random_music.ips',
'fast_doors.ips', 'elevators_speed.ips',
'spinjumprestart.ips', 'rando_speed.ips', 'No_Music', 'AimAnyButton.ips',
'max_ammo_display.ips', 'supermetroid_msu1.ips', 'Infinite_Space_Jump',
'refill_before_save.ips', 'relaxed_round_robin_cf.ips'])
parser.add_argument('--missileQty', '-m',
help="quantity of missiles",
dest='missileQty', nargs='?', default=3,
@ -177,9 +170,6 @@ class VariaRandomizer:
parser.add_argument('--scavRandomized',
help="For Scavenger split, decide whether mandatory major locs will have non-vanilla items",
dest='scavRandomized', nargs='?', const=True, default=False)
parser.add_argument('--scavEscape',
help="For Scavenger split, decide whether escape sequence shall be triggered as soon as the hunt is over",
dest='scavEscape', nargs='?', const=True, default=False)
parser.add_argument('--suitsRestriction',
help="no suits in early game",
dest='suitsRestriction', nargs='?', const=True, default=False)
@ -235,46 +225,11 @@ class VariaRandomizer:
parser.add_argument('--race', help="Race mode magic number, between 1 and 65535", dest='raceMagic',
type=int)
parser.add_argument('--vcr', help="Generate VCR output file", dest='vcr', action='store_true')
parser.add_argument('--palette', help="Randomize the palettes", dest='palette', action='store_true')
parser.add_argument('--individual_suit_shift', help="palette param", action='store_true',
dest='individual_suit_shift', default=False)
parser.add_argument('--individual_tileset_shift', help="palette param", action='store_true',
dest='individual_tileset_shift', default=False)
parser.add_argument('--no_match_ship_and_power', help="palette param", action='store_false',
dest='match_ship_and_power', default=True)
parser.add_argument('--seperate_enemy_palette_groups', help="palette param", action='store_true',
dest='seperate_enemy_palette_groups', default=False)
parser.add_argument('--no_match_room_shift_with_boss', help="palette param", action='store_false',
dest='match_room_shift_with_boss', default=True)
parser.add_argument('--no_shift_tileset_palette', help="palette param", action='store_false',
dest='shift_tileset_palette', default=True)
parser.add_argument('--no_shift_boss_palettes', help="palette param", action='store_false',
dest='shift_boss_palettes', default=True)
parser.add_argument('--no_shift_suit_palettes', help="palette param", action='store_false',
dest='shift_suit_palettes', default=True)
parser.add_argument('--no_shift_enemy_palettes', help="palette param", action='store_false',
dest='shift_enemy_palettes', default=True)
parser.add_argument('--no_shift_beam_palettes', help="palette param", action='store_false',
dest='shift_beam_palettes', default=True)
parser.add_argument('--no_shift_ship_palette', help="palette param", action='store_false',
dest='shift_ship_palette', default=True)
parser.add_argument('--min_degree', help="min hue shift", dest='min_degree', nargs='?', default=-180, type=int)
parser.add_argument('--max_degree', help="max hue shift", dest='max_degree', nargs='?', default=180, type=int)
parser.add_argument('--no_global_shift', help="", action='store_false', dest='global_shift', default=True)
parser.add_argument('--invert', help="invert color range", dest='invert', action='store_true', default=False)
parser.add_argument('--no_blue_door_palette', help="palette param", action='store_true',
dest='no_blue_door_palette', default=False)
parser.add_argument('--ext_stats', help="dump extended stats SQL", nargs='?', default=None, dest='extStatsFilename')
parser.add_argument('--randoPreset', help="rando preset file", dest="randoPreset", nargs='?', default=None)
parser.add_argument('--fakeRandoPreset', help="for prog speed stats", dest="fakeRandoPreset", nargs='?', default=None)
parser.add_argument('--plandoRando', help="json string with already placed items/locs", dest="plandoRando",
nargs='?', default=None)
parser.add_argument('--sprite', help='use a custom sprite for Samus', dest='sprite', default=None)
parser.add_argument('--no_spin_attack', help='when using a custom sprite, use the same animation for screw attack with or without Space Jump', dest='noSpinAttack', action='store_true', default=False)
parser.add_argument('--customItemNames', help='add custom item names for some of them, related to the custom sprite',
dest='customItemNames', action='store_true', default=False)
parser.add_argument('--ship', help='use a custom sprite for Samus ship', dest='ship', default=None)
parser.add_argument('--seedIps', help='ips generated from previous seed', dest='seedIps', default=None)
parser.add_argument('--jm,', help="display data used by jm for its stats", dest='jm', action='store_true', default=False)
parser.add_argument('--doorsColorsRando', help='randomize color of colored doors', dest='doorsColorsRando',
nargs='?', const=True, default=False)
@ -283,6 +238,17 @@ class VariaRandomizer:
parser.add_argument('--logic', help='logic to use', dest='logic', nargs='?', default="varia", choices=["varia", "rotation"])
parser.add_argument('--hud', help='Enable VARIA hud', dest='hud',
nargs='?', const=True, default=False)
parser.add_argument('--objective',
help="objectives to open G4",
dest='objective', nargs='?', default=[], action='append',
choices=Objectives.getAllGoals()+["random"]+[str(i) for i in range(6)])
parser.add_argument('--objectiveList', help="list to choose from when random",
dest='objectiveList', nargs='?', default=None)
parser.add_argument('--tourian', help="Tourian mode",
dest='tourian', nargs='?', default='Vanilla',
choices=tourians+['random'])
parser.add_argument('--tourianList', help="list to choose from when random",
dest='tourianList', nargs='?', default=None)
def __init__(self, world, rom, player):
# parse args
@ -293,15 +259,13 @@ class VariaRandomizer:
# args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key)
if args.output is None and args.rom is None:
print("Need --output or --rom parameter")
sys.exit(-1)
elif args.output is not None and args.rom is not None:
print("Can't have both --output and --rom parameters")
sys.exit(-1)
raise Exception("Need --output or --rom parameter")
if args.plandoRando != None and args.output == None:
print("plandoRando param requires output param")
sys.exit(-1)
elif args.output is not None and args.rom is not None:
raise Exception("Can't have both --output and --rom parameters")
if args.plandoRando is not None and args.output is None:
raise Exception("plandoRando param requires output param")
log.init(args.debug)
logger = log.get('Rando')
@ -320,7 +284,7 @@ class VariaRandomizer:
if argDict[arg] not in okValues:
argDict[arg] = value
self.forcedArgs[webArg if webArg != None else arg] = webValue if webValue != None else value
self.forcedArgs[webArg if webArg is not None else arg] = webValue if webValue is not None else value
# print(msg)
# optErrMsgs.append(msg)
@ -368,8 +332,7 @@ class VariaRandomizer:
if args.raceMagic is not None:
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
print("Invalid magic")
sys.exit(-1)
raise Exception("Invalid magic")
# if no max diff, set it very high
if args.maxDifficulty:
@ -401,6 +364,11 @@ class VariaRandomizer:
(_, progDiff) = randomMulti(args.__dict__, "progressionDifficulty", progDiffs)
(majorsSplitRandom, args.majorsSplit) = randomMulti(args.__dict__, "majorsSplit", majorsSplits)
(_, self.gravityBehaviour) = randomMulti(args.__dict__, "gravityBehaviour", gravityBehaviours)
(_, args.tourian) = randomMulti(args.__dict__, "tourian", tourians)
(areaRandom, args.area) = randomMulti(args.__dict__, "area", areaRandomizations)
areaRandomization = args.area in ['light', 'full']
lightArea = args.area == 'light'
if args.minDifficulty:
minDifficulty = text2diff[args.minDifficulty]
if progSpeed != "speedrun":
@ -408,7 +376,7 @@ class VariaRandomizer:
else:
minDifficulty = 0
if args.area == True and args.bosses == True and args.minimizerN is not None:
if areaRandomization == True and args.bosses == True and args.minimizerN is not None:
forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full", altValue='FullWithHUD')
if args.minimizerN == "random":
self.minimizerN = random.randint(30, 60)
@ -417,11 +385,6 @@ class VariaRandomizer:
self.minimizerN = int(args.minimizerN)
else:
self.minimizerN = None
areaRandom = False
if args.area == 'random':
areaRandom = True
args.area = bool(random.getrandbits(1))
logger.debug("area: {}".format(args.area))
doorsColorsRandom = False
if args.doorsColorsRando == 'random':
@ -443,7 +406,7 @@ class VariaRandomizer:
forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
if args.suitsRestriction == 'random':
if args.morphPlacement == 'late' and args.area == True:
if args.morphPlacement == 'late' and areaRandomization == True:
forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
else:
args.suitsRestriction = bool(random.getrandbits(1))
@ -453,7 +416,7 @@ class VariaRandomizer:
args.hideItems = bool(random.getrandbits(1))
if args.morphPlacement == 'random':
if args.morphPlacementList != None:
if args.morphPlacementList is not None:
morphPlacements = args.morphPlacementList.split(',')
args.morphPlacement = random.choice(morphPlacements)
# Scavenger Hunt constraints
@ -465,9 +428,10 @@ class VariaRandomizer:
forceArg('startLocation', "Landing Site", "Start Location forced to Landing Site because of Scavenger mode")
if args.morphPlacement == 'late':
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal instead of late")
if args.scavEscape == True:
forceArg('escapeRando', True, "'Escape randomization' forced to on", webValue='on')
forceArg('noRemoveEscapeEnemies', True, "Enemies enabled during escape sequence", webArg='removeEscapeEnemies', webValue='off')
# use escape rando for auto escape trigger
if args.tourian == 'Disabled':
forceArg('escapeRando', True, "'Escape randomization' forced to on", webValue='on')
forceArg('noRemoveEscapeEnemies', True, "Enemies enabled during escape sequence", webArg='removeEscapeEnemies', webValue='off')
# random fill makes certain options unavailable
if (progSpeed == 'speedrun' or progSpeed == 'basic') and args.majorsSplit != 'Scavenger':
forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal")
@ -485,19 +449,17 @@ class VariaRandomizer:
forceArg('noLayout', False, "'Anti-softlock layout patches' forced to on", webValue='on')
forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
forceArg('areaLayoutBase', False, "'Additional layout patches for easier navigation' forced to on", webValue='on')
possibleStartAPs, reasons = GraphUtils.getPossibleStartAPs(args.area, self.maxDifficulty, args.morphPlacement, self.player)
possibleStartAPs, reasons = GraphUtils.getPossibleStartAPs(areaRandomization, self.maxDifficulty, args.morphPlacement, self.player)
if args.startLocation == 'random':
if args.startLocationList != None:
# to be able to give the list in jm we had to replace ' ' with '_', do the opposite operation
startLocationList = args.startLocationList.replace('_', ' ')
startLocationList = startLocationList.split(',')
if args.startLocationList is not None:
startLocationList = args.startLocationList.split(',')
# intersection between user whishes and reality
possibleStartAPs = sorted(list(set(possibleStartAPs).intersection(set(startLocationList))))
if len(possibleStartAPs) == 0:
optErrMsgs += ["%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList]
optErrMsgs.append('Invalid start locations list with your settings.')
dumpErrorMsgs(args.output, optErrMsgs)
sys.exit(-1)
#optErrMsgs += ["%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList]
raise Exception("Invalid start locations list with your settings." +
"%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList)
#dumpErrorMsgs(args.output, optErrMsgs)
args.startLocation = random.choice(possibleStartAPs)
elif args.startLocation not in possibleStartAPs:
args.startLocation = 'Landing Site'
@ -505,7 +467,6 @@ class VariaRandomizer:
#optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
#optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
#dumpErrorMsgs(args.output, optErrMsgs)
#sys.exit(-1)
ap = getAccessPoint(args.startLocation)
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")
@ -517,8 +478,7 @@ class VariaRandomizer:
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for custom start location")
if args.majorsSplit == 'Chozo' and args.morphPlacement == "late":
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for Chozo")
#if args.patchOnly == False:
# print("SEED: " + str(self.seed))
#print("SEED: " + str(self.seed))
# fill restrictions dict
restrictions = { 'Suits' : args.suitsRestriction, 'Morph' : args.morphPlacement, "doors": "normal" if not args.doorsColorsRando else "late" }
@ -527,7 +487,8 @@ class VariaRandomizer:
scavNumLocs = int(args.scavNumLocs)
if scavNumLocs == 0:
scavNumLocs = random.randint(4,16)
restrictions["ScavengerParams"] = {'numLocs':scavNumLocs, 'vanillaItems':not args.scavRandomized, 'escape': args.scavEscape}
restrictions["ScavengerParams"] = {'numLocs':scavNumLocs, 'vanillaItems':not args.scavRandomized}
restrictions["EscapeTrigger"] = args.tourian == 'Disabled'
seedCode = 'X'
if majorsSplitRandom == False:
if restrictions['MajorMinor'] == 'Full':
@ -542,16 +503,12 @@ class VariaRandomizer:
seedCode = 'B'+seedCode
if args.doorsColorsRando == True and doorsColorsRandom == False:
seedCode = 'D'+seedCode
if args.area == True and areaRandom == False:
if areaRandomization == True and areaRandom == False:
seedCode = 'A'+seedCode
# output ROM name
#if args.patchOnly == False:
# self.fileName = 'VARIA_Randomizer_' + seedCode + str(self.seed) + '_' + preset
# if args.progressionSpeed != "random":
# self.fileName += "_" + args.progressionSpeed
#else:
# self.fileName = 'VARIA' # TODO : find better way to name the file (argument?)
#fileName = 'VARIA_Randomizer_' + seedCode + str(seed) + '_' + preset
#if args.progressionSpeed != "random":
# fileName += "_" + args.progressionSpeed
self.fileName = output_path
seedName = self.fileName
if args.directory != '.':
@ -572,8 +529,12 @@ class VariaRandomizer:
RomPatches.ActivePatches[self.player] += RomPatches.VariaTweaks
if self.minimizerN is not None:
RomPatches.ActivePatches[self.player].append(RomPatches.NoGadoras)
if args.minimizerTourian == True:
RomPatches.ActivePatches[self.player] += RomPatches.MinimizerTourian
if args.tourian == 'Fast':
RomPatches.ActivePatches[self.player] += RomPatches.MinimizerTourian
elif args.tourian == 'Disabled':
RomPatches.ActivePatches[self.player].append(RomPatches.NoTourian)
if 'relaxed_round_robin_cf.ips' in args.patches:
RomPatches.ActivePatches[self.player].append(RomPatches.RoundRobinCF)
missileQty = float(args.missileQty)
superQty = float(args.superQty)
powerBombQty = float(args.powerBombQty)
@ -588,10 +549,8 @@ class VariaRandomizer:
if minorQty < 1:
minorQty = random.randint(25, 100)
if self.energyQty == 'random':
if args.energyQtyList != None:
# with jm can't have a list with space in it
energyQtyList = args.energyQtyList.replace('_', ' ')
energyQties = energyQtyList.split(',')
if args.energyQtyList is not None:
energyQties = args.energyQtyList.split(',')
self.energyQty = random.choice(energyQties)
if self.energyQty == 'ultra sparse':
# add nerfed rainbow beam patch
@ -620,31 +579,24 @@ class VariaRandomizer:
self.ctrlDict = { getattr(ctrl, button) : button for button in ctrlButton }
args.moonWalk = ctrl.Moonwalk
PlandoOptions = None
plandoSettings = None
if args.plandoRando is not None:
plandoRando = json.loads(args.plandoRando)
forceArg('progressionSpeed', 'speedrun', "'Progression Speed' forced to speedrun")
progSpeed = 'speedrun'
forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full")
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal")
forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal")
progDiff = 'normal'
args.plandoRando = json.loads(args.plandoRando)
RomPatches.ActivePatches[self.player] = args.plandoRando["patches"]
DoorsManager.unserialize(args.plandoRando["doors"])
PlandoOptions = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']}
RomPatches.ActivePatches = plandoRando["patches"]
DoorsManager.unserialize(plandoRando["doors"])
plandoSettings = {"locsItems": plandoRando['locsItems'], "forbiddenItems": plandoRando['forbiddenItems']}
randoSettings = RandoSettings(self.maxDifficulty, progSpeed, progDiff, qty,
restrictions, args.superFun, args.runtimeLimit_s,
PlandoOptions, minDifficulty)
# print some parameters for jm's stats
if args.jm == True:
print("startLocation:{}".format(args.startLocation))
print("progressionSpeed:{}".format(progSpeed))
print("majorsSplit:{}".format(args.majorsSplit))
print("morphPlacement:{}".format(args.morphPlacement))
plandoSettings, minDifficulty)
dotFile = None
if args.area == True:
if areaRandomization == True:
if args.dot == True:
dotFile = args.directory + '/' + seedName + '.dot'
RomPatches.ActivePatches[self.player] += RomPatches.AreaBaseSet
@ -652,49 +604,84 @@ class VariaRandomizer:
RomPatches.ActivePatches[self.player] += RomPatches.AreaComfortSet
if args.doorsColorsRando == True:
RomPatches.ActivePatches[self.player].append(RomPatches.RedDoorsMissileOnly)
graphSettings = GraphSettings(args.startLocation, args.area, args.lightArea, args.bosses,
args.escapeRando, self.minimizerN, dotFile, args.doorsColorsRando, args.allowGreyDoors,
args.plandoRando["transitions"] if args.plandoRando != None else None)
graphSettings = GraphSettings(self.player, args.startLocation, areaRandomization, lightArea, args.bosses,
args.escapeRando, self.minimizerN, dotFile,
args.doorsColorsRando, args.allowGreyDoors, args.tourian,
plandoRando["transitions"] if plandoSettings is not None else None)
if args.plandoRando is None:
if plandoSettings is None:
DoorsManager.setDoorsColor(self.player)
self.escapeAttr = None
if args.patchOnly == False:
try:
self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player)
self.container = self.randoExec.randomize()
# if we couldn't find an area layout then the escape graph is not created either
# and getDoorConnections will crash if random escape is activated.
stuck = False
if not stuck or args.vcr == True:
self.doors = GraphUtils.getDoorConnections(self.randoExec.areaGraph,
args.area, args.bosses,
args.escapeRando)
escapeAttr = self.randoExec.areaGraph.EscapeAttributes if args.escapeRando else None
if escapeAttr is not None:
escapeAttr['patches'] = []
if args.noRemoveEscapeEnemies == True:
escapeAttr['patches'].append("Escape_Rando_Enable_Enemies")
if args.scavEscape == True:
escapeAttr['patches'].append('Escape_Scavenger')
except Exception as e:
import traceback
traceback.print_exc(file=sys.stdout)
dumpErrorMsg(args.output, "Error: {}".format(e))
sys.exit(-1)
if plandoSettings is None:
self.objectivesManager = Objectives(self.player, args.tourian != 'Disabled', randoSettings)
addedObjectives = 0
if args.majorsSplit == "Scavenger":
self.objectivesManager.setScavengerHunt()
addedObjectives = 1
if args.objective:
try:
nbObjectives = int(args.objective[0])
except:
nbObjectives = 0 if "random" in args.objective else None
if nbObjectives is not None:
availableObjectives = args.objectiveList.split(',') if args.objectiveList is not None else objectives
if nbObjectives == 0:
nbObjectives = random.randint(1, min(Objectives.maxActiveGoals, len(availableObjectives)))
self.objectivesManager.setRandom(nbObjectives, availableObjectives)
else:
maxActiveGoals = Objectives.maxActiveGoals - addedObjectives
if len(args.objective) > maxActiveGoals:
args.objective = args.objective[0:maxActiveGoals]
for goal in args.objective:
self.objectivesManager.addGoal(goal)
self.objectivesManager.expandGoals()
else:
if not (args.majorsSplit == "Scavenger" and args.tourian == 'Disabled'):
self.objectivesManager.setVanilla()
if len(self.objectivesManager.activeGoals) == 0:
self.objectivesManager.addGoal('nothing')
if any(goal for goal in self.objectivesManager.activeGoals if goal.area is not None):
forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on')
else:
stuck = False
itemLocs = []
progItemLocs = None
args.tourian = plandoRando["tourian"]
self.objectivesManager = Objectives(self.player, args.tourian != 'Disabled')
for goal in plandoRando["objectives"]:
self.objectivesManager.addGoal(goal)
# print some parameters for jm's stats
#if args.jm == True:
# print("startLocation:{}".format(args.startLocation))
# print("progressionSpeed:{}".format(progSpeed))
# print("majorsSplit:{}".format(args.majorsSplit))
# print("morphPlacement:{}".format(args.morphPlacement))
# print("gravity:{}".format(gravityBehaviour))
# print("maxDifficulty:{}".format(maxDifficulty))
# print("tourian:{}".format(args.tourian))
# print("objectives:{}".format([g.name for g in Objectives.activeGoals]))
# print("energyQty:{}".format(energyQty))
#try:
self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player)
self.container = self.randoExec.randomize()
# if we couldn't find an area layout then the escape graph is not created either
# and getDoorConnections will crash if random escape is activated.
stuck = False
if not stuck or args.vcr == True:
self.escapeAttr = self.randoExec.areaGraph.EscapeAttributes if args.escapeRando else None
if self.escapeAttr is not None:
self.escapeAttr['patches'] = []
if args.noRemoveEscapeEnemies == True:
self.escapeAttr['patches'].append("Escape_Rando_Enable_Enemies")
#except Exception as e:
# import traceback
# traceback.print_exc(file=sys.stdout)
# dumpErrorMsg(args.output, "Error: {}".format(e))
if stuck == True:
dumpErrorMsg(args.output, self.randoExec.errorMsg)
print("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
# in vcr mode we still want the seed to be generated to analyze it
if args.vcr == False:
sys.exit(-1)
#if args.patchOnly == False:
# randoExec.postProcessItemLocs(itemLocs, args.hideItems)
#dumpErrorMsg(args.output, self.randoExec.errorMsg)
raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None):
args = self.args
@ -722,13 +709,12 @@ class VariaRandomizer:
# for loc in sorted(locsItems.keys()):
# print('{:>50}: {:>16} '.format(loc, locsItems[loc]))
# if args.plandoRando != None:
# if plandoSettings is not None:
# with open(args.output, 'w') as jsonFile:
# json.dump({"itemLocs": [il.json() for il in itemLocs], "errorMsg": randoExec.errorMsg}, jsonFile)
# sys.exit(0)
# # generate extended stats
# if args.extStatsFilename != None:
# if args.extStatsFilename is not None:
# with open(args.extStatsFilename, 'a') as extStatsFile:
# skillPreset = os.path.splitext(os.path.basename(args.paramsFileName))[0]
# if args.fakeRandoPreset is not None:
@ -738,96 +724,64 @@ class VariaRandomizer:
# db.DB.dumpExtStatsItems(skillPreset, randoPreset, locsItems, extStatsFile)
try:
if args.hud == True or args.majorsSplit == "FullWithHUD":
args.patches.append("varia_hud.ips")
if args.debug == True:
args.patches.append("Disable_Clear_Save_Boot")
patcherSettings = {
"isPlando": False,
"majorsSplit": args.majorsSplit,
"startLocation": args.startLocation,
"optionalPatches": args.patches,
"layout": not args.noLayout,
"suitsMode": args.gravityBehaviour,
"area": args.area in ['light', 'full'],
"boss": args.bosses,
"areaLayout": not args.areaLayoutBase,
"variaTweaks": not args.noVariaTweaks,
"nerfedCharge": args.nerfedCharge,
"nerfedRainbowBeam": args.energyQty == 'ultra sparse',
"escapeAttr": self.escapeAttr,
"minimizerN": None, #minimizerN,
"tourian": args.tourian,
"doorsColorsRando": args.doorsColorsRando,
"vanillaObjectives": self.objectivesManager.isVanilla(),
"ctrlDict": self.ctrlDict,
"moonWalk": args.moonWalk,
"seed": self.seed,
"randoSettings": self.randoExec.randoSettings,
"doors": self.doors,
"displayedVersion": displayedVersion,
#"itemLocs": itemLocs,
#"progItemLocs": progItemLocs,
}
# args.rom is not None: generate local rom named filename.sfc with args.rom as source
# args.output is not None: generate local json named args.output
if args.rom is not None:
# patch local rom
romFileName = args.rom
shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(outputFilename, args.raceMagic, False, self.player)
romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player)
else:
romPatcher = RomPatcher(magic=args.raceMagic)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
if customPrePatchApply != None:
customPrePatchApply(romPatcher)
if args.hud == True or args.majorsSplit == "FullWithHUD":
args.patches.append("varia_hud.ips")
if args.patchOnly == False:
romPatcher.applyIPSPatches(args.startLocation, args.patches,
args.noLayout, self.gravityBehaviour,
args.area, args.bosses, args.areaLayoutBase,
args.noVariaTweaks, args.nerfedCharge, self.energyQty == 'ultra sparse',
self.escapeAttr, self.minimizerN, args.minimizerTourian,
args.doorsColorsRando)
else:
# from customizer permalink, apply previously generated seed ips first
if args.seedIps != None:
romPatcher.applyIPSPatch(args.seedIps)
romPatcher.addIPSPatches(args.patches)
# don't color randomize custom ships
args.shift_ship_palette = False
romPatcher.patchRom()
if customPostPatchApply != None:
customPostPatchApply(romPatcher)
# we have to write ips to ROM before doing our direct modifications which will rewrite some parts (like in credits),
# but in web mode we only want to generate a global ips at the end
# if args.rom != None:
# romPatcher.commitIPS()
if args.patchOnly == False:
# romPatcher.writeItemsLocs(itemLocs)
# romPatcher.writeSplitLocs(args.majorsSplit, itemLocs, progItemLocs)
romPatcher.writeItemsNumber()
romPatcher.writeSeed(self.seed) # lol if race mode
# romPatcher.writeSpoiler(itemLocs, progItemLocs)
# romPatcher.writeRandoSettings(self.randoExec.randoSettings, itemLocs)
romPatcher.writeDoorConnections(self.doors)
romPatcher.writeVersion(displayedVersion)
if self.ctrlDict is not None:
romPatcher.writeControls(self.ctrlDict)
if args.moonWalk == True:
romPatcher.enableMoonWalk()
if args.patchOnly == False:
romPatcher.writeMagic()
romPatcher.writeMajorsSplit(args.majorsSplit)
# if args.palette == True:
# paletteSettings = {
# "global_shift": None,
# "individual_suit_shift": None,
# "individual_tileset_shift": None,
# "match_ship_and_power": None,
# "seperate_enemy_palette_groups": None,
# "match_room_shift_with_boss": None,
# "shift_tileset_palette": None,
# "shift_boss_palettes": None,
# "shift_suit_palettes": None,
# "shift_enemy_palettes": None,
# "shift_beam_palettes": None,
# "shift_ship_palette": None,
# "min_degree": None,
# "max_degree": None,
# "invert": None,
# "no_blue_door_palette": None
# }
# for param in paletteSettings:
# paletteSettings[param] = getattr(args, param)
# PaletteRando(romPatcher, paletteSettings, args.sprite).randomize()
# web mode, generate only one ips at the end
if args.rom == None:
romPatcher.commitIPS()
romPatcher.end()
if args.patchOnly == False:
if len(optErrMsgs) > 0:
# optErrMsgs.append(randoExec.errorMsg)
msg = joinErrorMsgs(optErrMsgs)
else:
# msg = randoExec.errorMsg
msg = ''
if len(optErrMsgs) > 0:
#optErrMsgs.append(randoExec.errorMsg)
msg = joinErrorMsgs(optErrMsgs)
else:
#msg = randoExec.errorMsg
msg = ''
if args.rom is None: # web mode
data = romPatcher.romFile.data
self.fileName = '{}.sfc'.format(self.fileName)
@ -845,9 +799,8 @@ class VariaRandomizer:
except Exception as e:
import traceback
traceback.print_exc(file=sys.stdout)
msg = "Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e)
dumpErrorMsg(args.output, msg)
sys.exit(-1)
raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e))
#dumpErrorMsg(args.output, msg)
# if stuck == True:
# print("Rom generated for debug purpose: {}".format(self.fileName))

View File

@ -0,0 +1,72 @@
from .rom import snes_to_pc, pc_to_snes
class Byte(object):
def __init__(self, value):
self.value = value
def expand(self):
return [self.value]
class Word(object):
def __init__(self, value):
self.value = value
def expand(self):
return [self.value, self.value+1]
class Long(object):
def __init__(self, value):
self.value = value
def expand(self):
return [self.value, self.value+1, self.value+2]
class ValueSingle(object):
def __init__(self, value, storage=Word):
self.value = snes_to_pc(value)
self.storage = storage
def getOne(self):
return self.value
def getAll(self):
return [self.value]
def getWeb(self):
return self.storage(self.value).expand()
class ValueList(object):
def __init__(self, values, storage=Word):
self.values = [snes_to_pc(value) for value in values]
self.storage = storage
def getOne(self):
return self.values[0]
def getAll(self):
return self.values
def getWeb(self):
out = []
for value in self.values:
out += self.storage(value).expand()
return out
class ValueRange(object):
def __init__(self, start, length=-1, end=-1):
self.start = snes_to_pc(start)
if length != -1:
self.end = self.start + length
self.length = length
else:
self.end = snes_to_pc(end)
self.length = self.end - self.start
def getOne(self):
return self.start
def getAll(self):
return [self.start+i for i in range(self.length)]
def getWeb(self):
return [self.start, self.end]

View File

@ -0,0 +1,51 @@
from .addressTypes import ValueList, ValueSingle, ValueRange, Byte, Word, Long
from .objectivesAddresses import objectivesAddr
# TODO::add patches
class Addresses(object):
@staticmethod
def getOne(key):
value = Addresses.addresses[key]
return value.getOne()
@staticmethod
def getAll(key):
value = Addresses.addresses[key]
return value.getAll()
@staticmethod
def getWeb(key):
value = Addresses.addresses[key]
return value.getWeb()
@staticmethod
def getRange(key):
value = Addresses.addresses[key]
return value.getWeb()
addresses = {
'totalItems': ValueList([0x8BE656, 0x8BE6B3], storage=Byte),
'majorsSplit': ValueSingle(0x82fb6c, storage=Byte),
# scavenger hunt items list (17 prog items (including ridley) + hunt over + terminator, each is a word)
'scavengerOrder': ValueRange(0xA1F5D8, length=(17+1+1)*2),
'plandoAddresses': ValueRange(0xdee000, length=128),
'plandoTransitions': ValueSingle(0xdee100),
'escapeTimer': ValueSingle(0x809e21),
'escapeTimerTable': ValueSingle(0xA1F0AA),
'startAP': ValueSingle(0xa1f200),
'customDoorsAsm': ValueSingle(0x8ff800),
'locIdsByArea': ValueRange(0xA1F568, end=0xA1F5D7),
'plmSpawnTable': ValueSingle(0x8fe9a0),
'plmSpawnRoomTable': ValueSingle(0x8ff000),
'moonwalk': ValueSingle(0x81b35d),
'additionalETanks': ValueSingle(0xA1F470, storage=Byte),
'hellrunRate': ValueSingle(0x8DE387),
'BTtweaksHack1': ValueSingle(0x84ba6f+3),
'BTtweaksHack2': ValueSingle(0x84d33b+3),
# in intro_text.ips
'introText': ValueSingle(0x8cc389)
}
Addresses.addresses.update(objectivesAddr)

View File

@ -1,4 +1,4 @@
import itertools
import itertools, math
from ..utils.utils import range_union, openFile
@ -9,9 +9,14 @@ class IPS_Patch(object):
self.truncate_length = None
self.max_size = 0
if patchDict is not None:
recMaxSize = 0xffff
for addr, data in patchDict.items():
byteData = bytearray(data)
self.add_record(addr, byteData)
nrecs = int(math.ceil(float(len(data))/recMaxSize))
for i in range(nrecs):
start = i*recMaxSize
end = min((i+1)*recMaxSize, len(data))
byteData = bytearray(data[start:end])
self.add_record(addr+start, byteData)
def toDict(self):
ret = {}

View File

@ -0,0 +1,64 @@
from .addressTypes import ValueList, ValueSingle, ValueRange
# generated from asar output
# A1 start: A1FA80
objectivesAddr = {
# --- objectives checker functions: A1FA80 ---
'objectivesList': ValueSingle(0xA1FA80),
'objectiveEventsArray': ValueRange(0xA1FB1A, length=2*5),
'objective[kraid_is_dead]': ValueSingle(0xA1FBCE),
'objective[phantoon_is_dead]': ValueSingle(0xA1FBD6),
'objective[draygon_is_dead]': ValueSingle(0xA1FBDE),
'objective[ridley_is_dead]': ValueSingle(0xA1FBE6),
'objective[all_g4_dead]': ValueSingle(0xA1FBEE),
'objective[spore_spawn_is_dead]': ValueSingle(0xA1FC04),
'objective[botwoon_is_dead]': ValueSingle(0xA1FC0C),
'objective[crocomire_is_dead]': ValueSingle(0xA1FC14),
'objective[golden_torizo_is_dead]': ValueSingle(0xA1FC1C),
'objective[all_mini_bosses_dead]': ValueSingle(0xA1FC24),
'objective[scavenger_hunt_completed]': ValueSingle(0xA1FC3A),
'objective[boss_1_killed]': ValueSingle(0xA1FC7A),
'objective[boss_2_killed]': ValueSingle(0xA1FC83),
'objective[boss_3_killed]': ValueSingle(0xA1FC8C),
'objective[miniboss_1_killed]': ValueSingle(0xA1FC95),
'objective[miniboss_2_killed]': ValueSingle(0xA1FC9E),
'objective[miniboss_3_killed]': ValueSingle(0xA1FCA7),
'objective[collect_25_items]': ValueSingle(0xA1FCB0),
'__pct25': 0xA1FCB5,
'objective[collect_50_items]': ValueSingle(0xA1FCB8),
'__pct50': 0xA1FCBD,
'objective[collect_75_items]': ValueSingle(0xA1FCC0),
'__pct75': 0xA1FCC5,
'objective[collect_100_items]': ValueSingle(0xA1FCC8),
'__pct100': 0xA1FCCD,
'objective[nothing_objective]': ValueSingle(0xA1FCD0),
'objective[fish_tickled]': ValueSingle(0xA1FCF8),
'objective[orange_geemer]': ValueSingle(0xA1FD00),
'objective[shak_dead]': ValueSingle(0xA1FD08),
'itemsMask': ValueSingle(0xA1FD10),
'beamsMask': ValueSingle(0xA1FD12),
'objective[all_major_items]': ValueSingle(0xA1FD14),
'objective[crateria_cleared]': ValueSingle(0xA1FD2B),
'objective[green_brin_cleared]': ValueSingle(0xA1FD33),
'objective[red_brin_cleared]': ValueSingle(0xA1FD3B),
'objective[ws_cleared]': ValueSingle(0xA1FD43),
'objective[kraid_cleared]': ValueSingle(0xA1FD4B),
'objective[upper_norfair_cleared]': ValueSingle(0xA1FD53),
'objective[croc_cleared]': ValueSingle(0xA1FD5B),
'objective[lower_norfair_cleared]': ValueSingle(0xA1FD63),
'objective[west_maridia_cleared]': ValueSingle(0xA1FD6B),
'objective[east_maridia_cleared]': ValueSingle(0xA1FD73),
'objective[all_chozo_robots]': ValueSingle(0xA1FD7B),
'objective[visited_animals]': ValueSingle(0xA1FD9A),
'objective[king_cac_dead]': ValueSingle(0xA1FDE6),
# A1 end: A1FDEE
# Pause stuff: 82FB6D
# *** completed spritemaps: 82FE83
'objectivesSpritesOAM': ValueSingle(0x82FE83),
# 82 end: 82FEB0
'objectivesText': ValueSingle(0xB6F200),
}
_pctList = []
for pct in [25,50,75,100]:
_pctList.append(objectivesAddr['__pct%d' % pct])
del objectivesAddr['__pct%d' % pct]
objectivesAddr['totalItemsPercent'] = ValueList(_pctList)

View File

@ -18,17 +18,50 @@ def snes_to_pc(B):
return (A_1 << 16) | A_2
VANILLA_ROM_SIZE = 3145728
BANK_SIZE = 0x8000
class ROM(object):
def __init__(self, data={}):
self.address = 0
self.maxAddress = VANILLA_ROM_SIZE
def close(self):
pass
def seek(self, address):
if address > self.maxAddress:
self.maxAddress = address
self.address = address
def tell(self):
if self.address > self.maxAddress:
self.maxAddress = self.address
return self.address
def inc(self, n=1):
self.address += n
self.tell()
def read(self, byteCount):
pass
def readWord(self, address=None):
return self.readBytes(2, address)
def readByte(self, address=None):
return self.readBytes(1, address)
def readLong(self, address=None):
return self.readBytes(3, address)
def readBytes(self, size, address=None):
if address != None:
self.seek(address)
return int.from_bytes(self.read(size), byteorder='little')
def write(self, bytes):
pass
def writeWord(self, word, address=None):
self.writeBytes(word, 2, address)
@ -36,25 +69,45 @@ class ROM(object):
def writeByte(self, byte, address=None):
self.writeBytes(byte, 1, address)
def writeLong(self, lng, address=None):
self.writeBytes(lng, 3, address)
def writeBytes(self, value, size, address=None):
if address != None:
self.seek(address)
self.write(value.to_bytes(size, byteorder='little'))
def ipsPatch(self, ipsPatches):
pass
def fillToNextBank(self):
off = self.maxAddress % BANK_SIZE
if off > 0:
self.seek(self.maxAddress + BANK_SIZE - off - 1)
self.writeByte(0xff)
assert (self.maxAddress % BANK_SIZE) == 0
class RealROM(ROM):
def __init__(self, name):
super(RealROM, self).__init__()
self.romFile = open(name, "rb+")
self.address = 0
def seek(self, address):
self.address = address
super(RealROM, self).seek(address)
self.romFile.seek(address)
def tell(self):
self.address = self.romFile.tell()
return super(RealROM, self).tell()
def write(self, bytes):
self.romFile.write(bytes)
self.tell()
def read(self, byteCount):
return self.romFile.read(byteCount)
ret = self.romFile.read(byteCount)
self.tell()
return ret
def close(self):
self.romFile.close()

View File

@ -61,6 +61,8 @@ class RomPatches:
CrabShaftBlueDoor = 107
# wrap door from sand halls left to under botwoon
MaridiaSandWarp = 108
# Replace PB blocks at Aqueduct entrance with bomb blocks
AqueductBombBlocks = 109
## Minimizer Patches
NoGadoras = 200
TourianSpeedup = 201
@ -81,6 +83,12 @@ class RomPatches:
NerfedRainbowBeam = 1005
# Red doors open with one missile, and don't react to supers: part of door color rando
RedDoorsMissileOnly = 1006
# Escape auto-trigger on objectives completion (no Tourian)
NoTourian = 1007
# BT wakes up on its item instead of bombs
BombTorizoWake = 1008
# Round-Robin Crystal Flash patch
RoundRobinCF = 1009
### Hacks
# rotation hack
@ -103,11 +111,11 @@ class RomPatches:
AreaBaseSet = [ SingleChamberNoCrumble, AreaRandoGatesBase,
AreaRandoBlueDoors, AreaRandoMoreBlueDoors,
CrocBlueDoors, CrabShaftBlueDoor, MaridiaSandWarp ]
AreaComfortSet = [ AreaRandoGatesOther, SpongeBathBlueDoor, EastOceanPlatforms ]
AreaComfortSet = [ AreaRandoGatesOther, SpongeBathBlueDoor, EastOceanPlatforms, AqueductBombBlocks ]
AreaSet = AreaBaseSet + AreaComfortSet
# VARIA specific patch set
VariaTweaks = [ WsEtankPhantoonAlive, LNChozoSJCheckDisabled ]
VariaTweaks = [ WsEtankPhantoonAlive, LNChozoSJCheckDisabled, BombTorizoWake ]
# Tourian speedup in minimizer mode
MinimizerTourian = [ TourianSpeedup, OpenZebetites ]
@ -125,6 +133,6 @@ class RomPatches:
@staticmethod
def setDefaultPatches(startLocation):
# called by the isolver in seedless mode.
# activate only layout patch (the most common one), red tower blue doors and the startLocation's patches.
# activate only layout patch (the most common one), red tower blue doors, startLocation's patches and balanced suits.
from graph.graph_utils import GraphUtils
RomPatches.ActivePatches[0] = [RomPatches.RedTowerBlueDoors] + RomPatches.TotalLayout + GraphUtils.getGraphPatches(startLocation)
RomPatches.ActivePatches[0] = [RomPatches.RedTowerBlueDoors] + RomPatches.TotalLayout + GraphUtils.getGraphPatches(startLocation) + [RomPatches.NoGravityEnvProtection]

View File

@ -1,10 +1,15 @@
import os, random, re
import os, random, re, json
from math import ceil
from enum import IntFlag
from ..rando.Items import ItemManager
from ..rom.ips import IPS_Patch
from ..utils.doorsmanager import DoorsManager
from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses
from ..utils.doorsmanager import DoorsManager, IndicatorFlag
from ..utils.objectives import Objectives
from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas
from ..logic.logic import Logic
from ..rom.rom import RealROM, snes_to_pc, pc_to_snes
from ..rom.addresses import Addresses
from ..rom.rom_patches import RomPatches
from ..patches.patchaccess import PatchAccess
from ..utils.parameters import appDir
from ..utils import log
@ -20,9 +25,7 @@ class RomPatcher:
# faster MB cutscene transitions
'Mother_Brain_Cutscene_Edits',
# "Balanced" suit mode
'Removes_Gravity_Suit_heat_protection',
# door ASM to skip G4 cutscene when all 4 bosses are dead
'g4_skip.ips',
'Removes_Gravity_Suit_heat_protection'
],
# VARIA tweaks
'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'],
@ -30,20 +33,9 @@ class RomPatcher:
'Layout': ['dachora.ips', 'early_super_bridge.ips', 'high_jump.ips', 'moat.ips', 'spospo_save.ips',
'nova_boost_platform.ips', 'red_tower.ips', 'spazer.ips',
'brinstar_map_room.ips', 'kraid_save.ips', 'mission_impossible.ips'],
# comfort patches
'Optional': ['rando_speed.ips', 'Infinite_Space_Jump', 'refill_before_save.ips',
'spinjumprestart.ips', 'elevators_doors_speed.ips', 'No_Music', 'random_music.ips',
# animals
'animal_enemies.ips', 'animals.ips', 'draygonimals.ips',
'escapimals.ips', 'gameend.ips', 'grey_door_animals.ips',
'low_timer.ips', 'metalimals.ips', 'phantoonimals.ips', 'ridleyimals.ips',
'Escape_Animals_Change_Event', # ...end animals
# vanilla behaviour restore
'remove_elevators_doors_speed.ips',
'varia_hud.ips'],
# base patchset+optional layout for area rando
'Area': ['area_rando_layout.ips', 'door_transition.ips', 'area_rando_doors.ips',
'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips',
'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips', 'aqueduct_bomb_blocks.ips',
'crab_shaft.ips', 'Save_Crab_Shaft', 'Save_Main_Street', 'no_demo.ips'],
# patches for boss rando
'Bosses': ['door_transition.ips', 'no_demo.ips'],
@ -55,14 +47,16 @@ class RomPatcher:
'DoorsColors': ['beam_doors_plms.ips', 'beam_doors_gfx.ips', 'red_doors.ips']
}
def __init__(self, romFileName=None, magic=None, plando=False, player=0):
def __init__(self, settings=None, romFileName=None, magic=None, player=0):
self.log = log.get('RomPatcher')
self.settings = settings
self.romFileName = romFileName
self.patchAccess = PatchAccess()
self.race = None
self.romFile = RealROM(romFileName)
#if magic is not None:
# from rom.race_mode import RaceModePatcher
# self.race = RaceModePatcher(self, magic, plando)
# self.race = RaceModePatcher(self, magic)
# IPS_Patch objects list
self.ipsPatches = []
# loc name to alternate address. we still write to original
@ -79,18 +73,25 @@ class RomPatcher:
# get out of croc room: reload CRE
0x93ea: self.forceRoomCRE
}
self.patchAccess = PatchAccess()
self.player = player
def patchRom(self):
self.applyIPSPatches()
self.commitIPS()
def end(self):
self.romFile.fillToNextBank()
self.romFile.close()
def writeItemCode(self, item, visibility, address):
itemCode = ItemManager.getItemTypeCode(item, visibility)
self.writePlmWord(itemCode, address)
def writePlmWord(self, word, address):
if self.race is None:
self.romFile.writeWord(itemCode, address)
self.romFile.writeWord(word, address)
else:
self.race.writeItemCode(itemCode, address)
self.race.writePlmWord(word, address)
def getLocAddresses(self, loc):
ret = [loc.Address]
@ -115,23 +116,24 @@ class RomPatcher:
continue
self.writeItem(itemLoc)
if item.Category != 'Nothing':
self.nItems += 1
if not loc.restricted:
self.nItems += 1
if loc.Name == 'Morphing Ball':
self.patchMorphBallEye(item)
def writeSplitLocs(self, split, itemLocs, progItemLocs):
majChozoCheck = lambda itemLoc: itemLoc.Item.Class == split and itemLoc.Location.isClass(split)
fullCheck = lambda itemLoc: itemLoc.Location.Id is not None
fullCheck = lambda itemLoc: itemLoc.Location.Id is not None and itemLoc.Location.BossItemType is None
splitChecks = {
'Full': fullCheck,
'Scavenger': fullCheck,
'Major': majChozoCheck,
'Chozo': majChozoCheck,
'FullWithHUD': lambda itemLoc: itemLoc.Item.Category not in ['Energy', 'Ammo', 'Boss']
'FullWithHUD': lambda itemLoc: itemLoc.Item.Category not in ['Energy', 'Ammo', 'Boss', 'MiniBoss']
}
itemLocCheck = lambda itemLoc: itemLoc.Item.Category != "Nothing" and splitChecks[split](itemLoc)
for area,addr in locIdsByAreaAddresses.items():
locs = [il.Location for il in itemLocs if itemLocCheck(il) and il.Location.GraphArea == area]
locs = [il.Location for il in itemLocs if itemLocCheck(il) and il.Location.GraphArea == area and not il.Location.restricted]
self.log.debug("writeSplitLocs. area="+area)
self.log.debug(str([loc.Name for loc in locs]))
self.romFile.seek(addr)
@ -140,11 +142,14 @@ class RomPatcher:
self.romFile.writeByte(0xff)
if split == "Scavenger":
# write required major item order
self.romFile.seek(snes_to_pc(0xA1F5D8))
self.romFile.seek(Addresses.getOne('scavengerOrder'))
for itemLoc in progItemLocs:
self.romFile.writeWord((itemLoc.Location.Id << 8) | itemLoc.Location.HUD)
# bogus loc ID | "HUNT OVER" index
self.romFile.writeWord(0xff10)
self.romFile.writeWord(0xff11)
# fill remaining list with 0xFFFF to avoid issue with plandomizer having less items than in the base seed
for i in range(18-len(progItemLocs)):
self.romFile.writeWord(0xffff)
# trigger morph eye enemy on whatever item we put there,
# not just morph ball
@ -183,8 +188,8 @@ class RomPatcher:
operand = item.BeamBits
else:
operand = item.ItemBits
self.patchMorphBallCheck(0x1410E6, cat, comp, operand, branch) # eye main AI
self.patchMorphBallCheck(0x1468B2, cat, comp, operand, branch) # head main AI
self.patchMorphBallCheck(snes_to_pc(0xa890e6), cat, comp, operand, branch) # eye main AI
self.patchMorphBallCheck(snes_to_pc(0xa8e8b2), cat, comp, operand, branch) # head main AI
def patchMorphBallCheck(self, offset, cat, comp, operand, branch):
# actually patch enemy AI
@ -195,113 +200,111 @@ class RomPatcher:
def writeItemsNumber(self):
# write total number of actual items for item percentage patch (patch the patch)
for addr in [0x5E64E, 0x5E6AB]:
for addr in Addresses.getAll('totalItems'):
self.romFile.writeByte(self.nItems, addr)
# for X% collected items objectives, precompute values and write them in objectives functions
for percent, addr in zip([25, 50, 75, 100], Addresses.getAll('totalItemsPercent')):
self.romFile.writeWord(ceil((self.nItems * percent)/100), addr)
def addIPSPatches(self, patches):
for patchName in patches:
self.applyIPSPatch(patchName)
def writePlmTable(self, plms, area, bosses, startLocation):
# called when saving a plando
try:
if bosses == True or area == True:
plms.append('WS_Save_Blinking_Door')
doors = self.getStartDoors(plms, area, None)
self.writeDoorsColor(doors, self.player)
self.applyStartAP(startLocation, plms, doors)
self.applyPLMs(plms)
except Exception as e:
raise Exception("Error patching {}. ({})".format(self.romFileName, e))
def applyIPSPatches(self, startLocation="Landing Site",
optionalPatches=[], noLayout=False, suitsMode="Balanced",
area=False, bosses=False, areaLayoutBase=False,
noVariaTweaks=False, nerfedCharge=False, nerfedRainbowBeam=False,
escapeAttr=None, minimizerN=None, minimizerTourian=True,
doorsColorsRando=False):
def applyIPSPatches(self):
try:
# apply standard patches
stdPatches = []
plms = []
# apply race mode first because it fills the rom with a bunch of crap
if self.race is not None:
stdPatches.append('race_mode.ips')
stdPatches += RomPatcher.IPSPatches['Standard'][:]
if not self.settings["layout"]:
# when disabling anti softlock protection also disable doors indicators
stdPatches.remove('door_indicators_plms.ips')
if self.race is not None:
stdPatches.append('race_mode_credits.ips')
if suitsMode != "Balanced":
stdPatches.append('race_mode_post.ips')
if self.settings["suitsMode"] != "Balanced":
stdPatches.remove('Removes_Gravity_Suit_heat_protection')
if suitsMode == "Progressive":
if self.settings["suitsMode"] == "Progressive":
stdPatches.append('progressive_suits.ips')
if nerfedCharge == True:
if self.settings["nerfedCharge"] == True:
stdPatches.append('nerfed_charge.ips')
if nerfedRainbowBeam == True:
if self.settings["nerfedRainbowBeam"] == True:
stdPatches.append('nerfed_rainbow_beam.ips')
if bosses == True or area == True:
if self.settings["boss"] == True or self.settings["area"] == True:
stdPatches += ["WS_Main_Open_Grey", "WS_Save_Active"]
plms.append('WS_Save_Blinking_Door')
if bosses == True:
if self.settings["boss"] == True:
stdPatches.append("Phantoon_Eye_Door")
if area == True or doorsColorsRando == True:
if (self.settings["area"] == True
or self.settings["doorsColorsRando"] == True
or not GraphUtils.isStandardStart(self.settings["startLocation"])):
stdPatches.append("Enable_Backup_Saves")
if 'varia_hud.ips' in optionalPatches:
# varia hud has its own variant of g4_skip for scavenger mode,
# it can also make demos glitch out
stdPatches.remove("g4_skip.ips")
if 'varia_hud.ips' in self.settings["optionalPatches"]:
# varia hud can make demos glitch out
self.applyIPSPatch("no_demo.ips")
for patchName in stdPatches:
self.applyIPSPatch(patchName)
if noLayout == False:
if not self.settings["vanillaObjectives"]:
self.applyIPSPatch("Objectives_sfx")
# show objectives and Tourian status in a shortened intro sequence
# if not full vanilla objectives+tourian
if not self.settings["vanillaObjectives"] or self.settings["tourian"] != "Vanilla":
self.applyIPSPatch("Restore_Intro") # important to apply this after new_game.ips
self.applyIPSPatch("intro_text.ips")
if self.settings["layout"]:
# apply layout patches
for patchName in RomPatcher.IPSPatches['Layout']:
self.applyIPSPatch(patchName)
if noVariaTweaks == False:
if self.settings["variaTweaks"]:
# VARIA tweaks
for patchName in RomPatcher.IPSPatches['VariaTweaks']:
self.applyIPSPatch(patchName)
if (self.settings["majorsSplit"] == 'Scavenger'
and any(il for il in self.settings["progItemLocs"] if il.Location.Name == "Ridley")):
# ridley as scav loc
self.applyIPSPatch("Blinking[RidleyRoomIn]")
# apply optional patches
for patchName in optionalPatches:
if patchName in RomPatcher.IPSPatches['Optional']:
self.applyIPSPatch(patchName)
for patchName in self.settings["optionalPatches"]:
self.applyIPSPatch(patchName)
# random escape
if escapeAttr is not None:
if self.settings["escapeAttr"] is not None:
for patchName in RomPatcher.IPSPatches['Escape']:
self.applyIPSPatch(patchName)
# animals and timer
self.applyEscapeAttributes(escapeAttr, plms)
self.applyEscapeAttributes(self.settings["escapeAttr"], plms)
# apply area patches
if area == True:
if self.settings["area"] == True:
if not self.settings["areaLayout"]:
for p in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'aqueduct_bomb_blocks.ips']:
RomPatcher.IPSPatches['Area'].remove(p)
RomPatcher.IPSPatches['Area'].append('area_rando_layout_base.ips')
for patchName in RomPatcher.IPSPatches['Area']:
if areaLayoutBase == True and patchName in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips']:
continue
self.applyIPSPatch(patchName)
if areaLayoutBase == True:
self.applyIPSPatch('area_rando_layout_base.ips')
else:
self.applyIPSPatch('area_ids_alt.ips')
if bosses == True:
if self.settings["boss"] == True:
for patchName in RomPatcher.IPSPatches['Bosses']:
self.applyIPSPatch(patchName)
if minimizerN is not None:
if self.settings["minimizerN"] is not None:
self.applyIPSPatch('minimizer_bosses.ips')
if minimizerTourian == True:
for patchName in RomPatcher.IPSPatches['MinimizerTourian']:
self.applyIPSPatch(patchName)
doors = self.getStartDoors(plms, area, minimizerN)
if doorsColorsRando == True:
if self.settings["tourian"] == "Fast":
for patchName in RomPatcher.IPSPatches['MinimizerTourian']:
self.applyIPSPatch(patchName)
elif self.settings["tourian"] == "Disabled":
self.applyIPSPatch("Escape_Trigger")
doors = self.getStartDoors(plms, self.settings["area"], self.settings["minimizerN"])
if self.settings["doorsColorsRando"] == True:
for patchName in RomPatcher.IPSPatches['DoorsColors']:
self.applyIPSPatch(patchName)
self.writeDoorsColor(doors, self.player)
self.applyStartAP(startLocation, plms, doors)
if self.settings["layout"]:
self.writeDoorIndicators(plms, self.settings["area"], self.settings["doorsColorsRando"])
self.applyStartAP(self.settings["startLocation"], plms, doors)
self.applyPLMs(plms)
except Exception as e:
raise Exception("Error patching {}. ({})".format(self.romFileName, e))
@ -359,9 +362,9 @@ class RomPatcher:
if 'doors' in ap.Start:
doors += ap.Start['doors']
doors.append(0x0)
addr = 0x10F200
addr = Addresses.getOne('startAP')
patch = [w0, w1] + doors
assert (addr + len(patch)) < 0x10F210, "Stopped before new_game overwrite"
assert (addr + len(patch)) < addr + 0x10, "Stopped before new_game overwrite"
patchDict = {
'StartAP': {
addr: patch
@ -381,11 +384,22 @@ class RomPatcher:
# timer
escapeTimer = escapeAttr['Timer']
if escapeTimer is not None:
minute = int(escapeTimer / 60)
second = escapeTimer % 60
minute = int(minute / 10) * 16 + minute % 10
second = int(second / 10) * 16 + second % 10
patchDict = {'Escape_Timer': {0x1E21:[second, minute]}}
patchDict = { 'Escape_Timer': {} }
timerPatch = patchDict["Escape_Timer"]
def getTimerBytes(t):
minute = int(t / 60)
second = t % 60
minute = int(minute / 10) * 16 + minute % 10
second = int(second / 10) * 16 + second % 10
return [second, minute]
timerPatch[Addresses.getOne('escapeTimer')] = getTimerBytes(escapeTimer)
# timer table for Disabled Tourian escape
if 'TimerTable' in escapeAttr:
tableBytes = []
timerPatch[Addresses.getOne('escapeTimerTable')] = tableBytes
for area in graphAreas[1:-1]: # no Ceres or Tourian
t = escapeAttr['TimerTable'][area]
tableBytes += getTimerBytes(t)
self.applyIPSPatch('Escape_Timer', patchDict)
# animals door to open
if escapeAttr['Animals'] is not None:
@ -431,9 +445,9 @@ class RomPatcher:
for locName, locIndex in locList:
plmLocs[(k, locIndex)] = locName
# make two patches out of this dict
plmTblAddr = 0x7E9A0 # moves downwards
plmTblAddr = Addresses.getOne('plmSpawnTable') # moves downwards
plmPatchData = []
roomTblAddr = 0x7EC00 # moves upwards
roomTblAddr = Addresses.getOne('plmSpawnRoomTable') # moves upwards
roomPatchData = []
plmTblOffset = plmTblAddr
def appendPlmBytes(bytez):
@ -463,7 +477,7 @@ class RomPatcher:
addRoomPatchData(roomData)
# write room table terminator
addRoomPatchData([0x0] * 8)
assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap"
assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap. PLM table offset is 0x%x, Room table address is 0x%x" % (plmTblOffset,roomTblAddr)
patchDict = {
"PLM_Spawn_Tables" : {
plmTblAddr: plmPatchData,
@ -479,7 +493,7 @@ class RomPatcher:
random.seed(seed)
seedInfo = random.randint(0, 0xFFFF)
seedInfo2 = random.randint(0, 0xFFFF)
self.romFile.writeWord(seedInfo, 0x2FFF00)
self.romFile.writeWord(seedInfo, snes_to_pc(0xdfff00))
self.romFile.writeWord(seedInfo2)
def writeMagic(self):
@ -487,7 +501,7 @@ class RomPatcher:
self.race.writeMagic()
def writeMajorsSplit(self, majorsSplit):
address = 0x17B6C
address = Addresses.getOne('majorsSplit')
splits = {
'Chozo': 'Z',
'Major': 'M',
@ -528,7 +542,7 @@ class RomPatcher:
totalNothing = sum(1 for il in itemLocs if il.Accessible and il.Item.Category == 'Nothing')
totalEnergy = self.getItemQty(itemLocs, 'ETank')+self.getItemQty(itemLocs, 'Reserve')
totalMajors = max(totalItemLocs - totalEnergy - totalAmmo - totalNothing, 0)
address = 0x2736C0
address = snes_to_pc(0xceb6c0)
value = "{:>2}".format(totalItemLocs)
line = " ITEM LOCATIONS %s " % value
self.writeCreditsStringBig(address, line, top=True)
@ -606,7 +620,7 @@ class RomPatcher:
address += 0x40
# write ammo/energy pct
address = 0x273C40
address = snes_to_pc(0xcebc40)
(ammoPct, energyPct) = (int(self.getAmmoPct(dist)), int(100*totalEnergy/18))
line = " AVAILABLE AMMO {:>3}% ENERGY {:>3}%".format(ammoPct, energyPct)
self.writeCreditsStringBig(address, line, top=True)
@ -650,7 +664,7 @@ class RomPatcher:
return s
isRace = self.race is not None
startCreditAddress = 0x2f5240
startCreditAddress = snes_to_pc(0xded240)
address = startCreditAddress
if isRace:
addr = address - 0x40
@ -794,6 +808,12 @@ class RomPatcher:
else:
self.race.writeWordMagic(w)
def writeDoorTransition(self, roomPtr):
if self.race is None:
self.romFile.writeWord(roomPtr)
else:
self.race.writeDoorTransition(roomPtr)
# write area randomizer transitions to ROM
# doorConnections : a list of connections. each connection is a dictionary describing
# - where to write in the ROM :
@ -805,10 +825,10 @@ class RomPatcher:
# property shall point to this custom ASM.
# * if not, just write doorAsmPtr as the door property directly.
def writeDoorConnections(self, doorConnections):
asmAddress = 0x7F800
asmAddress = Addresses.getOne('customDoorsAsm')
for conn in doorConnections:
# write door ASM for transition doors (code and pointers)
# print('Writing door connection ' + conn['ID'])
# print('Writing door connection ' + conn['ID'] + ". doorPtr="+hex(doorPtr))
doorPtr = conn['DoorPtr']
roomPtr = conn['RoomPtr']
if doorPtr in self.doorConnectionSpecific:
@ -818,7 +838,7 @@ class RomPatcher:
self.romFile.seek(0x10000 + doorPtr)
# write room ptr
self.romFile.writeWord(roomPtr & 0xFFFF)
self.writeDoorTransition(roomPtr & 0xFFFF)
# write bitflag (if area switch we have to set bit 0x40, and remove it if same area)
self.romFile.writeByte(conn['bitFlag'])
@ -891,7 +911,7 @@ class RomPatcher:
# change BG table to avoid scrolling sky bug when transitioning to west ocean
def patchWestOcean(self, doorPtr):
self.romFile.writeWord(doorPtr, 0x7B7BB)
self.romFile.writeWord(doorPtr, snes_to_pc(0x8fb7bb))
# forces CRE graphics refresh when exiting kraid's or croc room
def forceRoomCRE(self, roomPtr, creFlag=0x2):
@ -934,7 +954,7 @@ class RomPatcher:
self.romFile.writeByte(RomPatcher.buttons[button][1])
def writePlandoAddresses(self, locations):
self.romFile.seek(0x2F6000)
self.romFile.seek(Addresses.getOne('plandoAddresses'))
for loc in locations:
self.romFile.writeWord(loc.Address & 0xFFFF)
@ -944,7 +964,7 @@ class RomPatcher:
self.romFile.writeWord(0xFFFF)
def writePlandoTransitions(self, transitions, doorsPtrs, maxTransitions):
self.romFile.seek(0x2F6100)
self.romFile.seek(Addresses.getOne('plandoTransitions'))
for (src, dest) in transitions:
self.romFile.writeWord(doorsPtrs[src])
@ -957,7 +977,14 @@ class RomPatcher:
def enableMoonWalk(self):
# replace STZ with STA since A is non-zero at this point
self.romFile.writeByte(0x8D, 0xB35D)
self.romFile.writeByte(0x8D, Addresses.getOne('moonwalk'))
def writeAdditionalETanks(self, additionalETanks):
self.romFile.writeByte(additionalETanks, Addresses.getOne("additionalETanks"))
def writeHellrunRate(self, hellrunRatePct):
hellrunRateVal = min(int(0x40*float(hellrunRatePct)/100.0), 0xff)
self.romFile.writeByte(hellrunRateVal, Addresses.getOne("hellrunRate"))
def setOamTile(self, nth, middle, newTile, y=0xFC):
# an oam entry is made of five bytes: (s000000 xxxxxxxxx) (yyyyyyyy) (YXpp000t tttttttt)
@ -976,8 +1003,8 @@ class RomPatcher:
# max 32 chars
# new oamlist address in free space at the end of bank 8C
self.romFile.writeWord(0xF3E9, 0x5a0e3)
self.romFile.writeWord(0xF3E9, 0x5a0e9)
self.romFile.writeWord(0xF3E9, snes_to_pc(0x8ba0e3))
self.romFile.writeWord(0xF3E9, snes_to_pc(0x8ba0e9))
# string length
versionLength = len(version)
@ -986,7 +1013,7 @@ class RomPatcher:
length = versionLength + rotationLength
else:
length = versionLength
self.romFile.writeWord(length, 0x0673e9)
self.romFile.writeWord(length, snes_to_pc(0x8cf3e9))
versionMiddle = int(versionLength / 2) + versionLength % 2
# oams
@ -998,8 +1025,64 @@ class RomPatcher:
for (i, char) in enumerate('rotation'):
self.setOamTile(i, rotationMiddle, char2tile[char], y=0x8e)
def writeDoorsColor(self, doors, player):
DoorsManager.writeDoorsColor(self.romFile, doors, player)
def writeDoorsColor(self, doorsStart, player):
if self.race is None:
DoorsManager.writeDoorsColor(self.romFile, doorsStart, player, self.romFile.writeWord)
else:
DoorsManager.writeDoorsColor(self.romFile, doorsStart, player, self.writePlmWord)
def writeDoorIndicators(self, plms, area, door):
indicatorFlags = IndicatorFlag.Standard | (IndicatorFlag.AreaRando if area else 0) | (IndicatorFlag.DoorRando if door else 0)
patchDict = self.patchAccess.getDictPatches()
additionalPLMs = self.patchAccess.getAdditionalPLMs()
def updateIndicatorPLM(door, doorType):
nonlocal additionalPLMs, patchDict
plmName = 'Indicator[%s]' % door
addPlm = False
if plmName in patchDict:
for addr,bytez in patchDict[plmName].items():
plmBytes = bytez
break
else:
plmBytes = additionalPLMs[plmName]['plm_bytes_list'][0]
addPlm = True
w = getWord(doorType)
plmBytes[0] = w[0]
plmBytes[1] = w[1]
return plmName, addPlm
indicatorPLMs = DoorsManager.getIndicatorPLMs(self.player, indicatorFlags)
for doorName,plmType in indicatorPLMs.items():
plmName,addPlm = updateIndicatorPLM(doorName, plmType)
if addPlm:
plms.append(plmName)
else:
self.applyIPSPatch(plmName)
def writeObjectives(self, itemLocs, tourian):
objectives = Objectives.objDict[self.player]
objectives.writeGoals(self.romFile)
objectives.writeIntroObjectives(self.romFile, tourian)
self.writeItemsMasks(itemLocs)
# hack bomb_torizo.ips to wake BT in all cases if necessary, ie chozo bots objective is on, and nothing at bombs
if objectives.isGoalActive("activate chozo robots") and RomPatches.has(RomPatches.BombTorizoWake):
bomb = next((il for il in itemLocs if il.Location.Name == "Bomb"), None)
if bomb is not None and bomb.Item.Category == "Nothing":
for addrName in ["BTtweaksHack1", "BTtweaksHack2"]:
self.romFile.seek(Addresses.getOne(addrName))
for b in [0xA9,0x00,0x00]: # LDA #$0000 ; set zero flag to wake BT
self.romFile.writeByte(b)
def writeItemsMasks(self, itemLocs):
# write items/beams masks for "collect all major" objective
itemsMask = 0
beamsMask = 0
for il in itemLocs:
if not il.Location.restricted:
item = il.Item
itemsMask |= item.ItemBits
beamsMask |= item.BeamBits
self.romFile.writeWord(itemsMask, Addresses.getOne('itemsMask'))
self.romFile.writeWord(beamsMask, Addresses.getOne('beamsMask'))
# tile number in tileset
char2tile = {
@ -1025,7 +1108,7 @@ class MessageBox(object):
# add 0x0c/0x06 to offsets as there's 12/6 bytes before the strings, string length is either 0x13/0x1a
self.offsets = {
'ETank': (0x2877f+0x0c, 0x13),
'ETank': (snes_to_pc(0x85877f)+0x0c, 0x13),
'Missile': (0x287bf+0x06, 0x1a),
'Super': (0x288bf+0x06, 0x1a),
'PowerBomb': (0x289bf+0x06, 0x1a),
@ -1088,3 +1171,266 @@ class MessageBox(object):
def updateAttr(self, byte, address):
self.rom.writeByte(byte, address)
class RomTypeForMusic(IntFlag):
VariaSeed = 1
AreaSeed = 2
BossSeed = 4
class MusicPatcher(object):
# rom: ROM object to patch
# romType: 0 if not varia seed, or bitwise or of RomTypeForMusic enum
# baseDir: directory containing all music data/descriptors/constraints
# constraintsFile: file to constraints JSON descriptor, relative to baseDir/constraints.
# if None, will be determined automatically from romType
def __init__(self, rom, romType,
baseDir=os.path.join(appDir, 'varia_custom_sprites', 'music'),
constraintsFile=None):
self.rom = rom
self.baseDir = baseDir
variaSeed = bool(romType & RomTypeForMusic.VariaSeed)
self.area = variaSeed and bool(romType & RomTypeForMusic.AreaSeed)
self.boss = variaSeed and bool(romType & RomTypeForMusic.BossSeed)
metaDir = os.path.join(baseDir, "_metadata")
constraintsDir = os.path.join(baseDir, "_constraints")
if constraintsFile is None:
constraintsFile = 'varia.json' if variaSeed else 'vanilla.json'
with open(os.path.join(constraintsDir, constraintsFile), 'r') as f:
self.constraints = json.load(f)
nspcInfoPath = os.path.join(baseDir, "nspc_metadata.json")
with open(nspcInfoPath, "r") as f:
nspcInfo = json.load(f)
self.nspcInfo = {}
for nspc,info in nspcInfo.items():
self.nspcInfo[self._nspc_path(nspc)] = info
self.allTracks = {}
self.vanillaTracks = None
for metaFile in os.listdir(metaDir):
metaPath = os.path.join(metaDir, metaFile)
if not metaPath.endswith(".json"):
continue
with open(metaPath, 'r') as f:
meta = json.load(f)
# will silently overwrite entries with same name, so avoid
# conflicting descriptor files ...
self.allTracks.update(meta)
if metaFile == "vanilla.json":
self.vanillaTracks = meta
assert self.vanillaTracks is not None, "MusicPatcher: missing vanilla JSON descriptor"
self.replaceableTracks = [track for track in self.vanillaTracks if track not in self.constraints['preserve'] and track not in self.constraints['discard']]
self.musicDataTableAddress = snes_to_pc(0x8FE7E4)
self.musicDataTableMaxSize = 45 # to avoid overwriting useful data in bank 8F
# tracks: dict with track name to replace as key, and replacing track name as value
# updateReferences: change room state headers and special tracks. may be False if you're patching a rom hack or something
# output: if not None, dump a JSON file with what was done
# replaced tracks must be in
# replaceableTracks, and new tracks must be in allTracks
# tracks not in the dict will be kept vanilla
# raise RuntimeError if not possible
def replace(self, tracks, updateReferences=True, output=None):
for track in tracks:
if track not in self.replaceableTracks:
raise RuntimeError("Cannot replace track %s" % track)
trackList = self._getTrackList(tracks)
replacedVanilla = [t for t in self.replaceableTracks if t in trackList and t not in tracks]
for van in replacedVanilla:
tracks[van] = van
# print("trackList="+str(trackList))
musicData = self._getMusicData(trackList)
# print("musicData="+str(musicData))
if len(musicData) > self.musicDataTableMaxSize:
raise RuntimeError("Music data table too long. %d entries, max is %d" % (len(musicData, self.musicDataTableMaxSize)))
musicDataAddresses = self._getMusicDataAddresses(musicData)
self._writeMusicData(musicDataAddresses)
self._writeMusicDataTable(musicData, musicDataAddresses)
if updateReferences == True:
self._updateReferences(trackList, musicData, tracks)
if output is not None:
self._dump(output, trackList, musicData, musicDataAddresses)
# compose a track list from vanilla tracks, replaced tracks, and constraints
def _getTrackList(self, replacedTracks):
trackList = set()
for track in self.vanillaTracks:
if track in replacedTracks:
trackList.add(replacedTracks[track])
elif track not in self.constraints['discard']:
trackList.add(track)
return list(trackList)
def _nspc_path(self, nspc_path):
return os.path.join(self.baseDir, nspc_path)
# get list of music data files to include in the ROM
# can contain empty entries, marked with a None, to account
# for fixed place data ('preserve' constraint)
def _getMusicData(self, trackList):
# first, make musicData the minimum size wrt preserved tracks
preservedTracks = {trackName:self.vanillaTracks[trackName] for trackName in self.constraints['preserve']}
preservedDataIndexes = [track['data_index'] for trackName,track in preservedTracks.items()]
musicData = [None]*(max(preservedDataIndexes)+1)
# fill preserved spots
for track in self.constraints['preserve']:
idx = self.vanillaTracks[track]['data_index']
nspc = self._nspc_path(self.vanillaTracks[track]['nspc_path'])
if nspc not in musicData:
musicData[idx] = nspc
# print("stored " + nspc + " at "+ str(idx))
# then fill data in remaining spots
idx = 0
for track in trackList:
previdx = idx
if track not in self.constraints['preserve']:
nspc = self._nspc_path(self.allTracks[track]['nspc_path'])
if nspc not in musicData:
for i in range(idx, len(musicData)):
# print("at " + str(i) + ": "+str(musicData[i]))
if musicData[i] is None:
musicData[i] = nspc
idx = i+1
break
if idx == previdx:
idx += 1
musicData.append(nspc)
# print("stored " + nspc + " at "+ str(idx))
return musicData
# get addresses to store each data file to. raise RuntimeError if not possible
# pretty dumb algorithm for now, just store data wherever possible,
# prioritizing first areas in usableSpace
# store data from end of usable space to make room for other data (for hacks for instance)
def _getMusicDataAddresses(self, musicData):
usableSpace = self.constraints['usable_space_ranges_pc']
musicDataAddresses = {}
for dataFile in musicData:
if dataFile is None:
continue
sz = os.path.getsize(dataFile)
blocks = self.nspcInfo[dataFile]['block_headers_offsets']
for r in usableSpace:
# find a suitable address so header words are not split across banks (header is 2 words)
addr = r['end'] - sz
def isCrossBank(off):
nonlocal addr
endBankOffset = pc_to_snes(addr+off+4) & 0x7fff
return endBankOffset == 1 or endBankOffset == 3
while addr >= r['start'] and any(isCrossBank(off) for off in blocks):
addr -= 1
if addr >= r['start']:
musicDataAddresses[dataFile] = addr
r['end'] = addr
break
if dataFile not in musicDataAddresses:
raise RuntimeError("Cannot find enough space to store music data file "+dataFile)
return musicDataAddresses
def _writeMusicData(self, musicDataAddresses):
for dataFile, addr in musicDataAddresses.items():
self.rom.seek(addr)
with open(dataFile, 'rb') as f:
self.rom.write(f.read())
def _writeMusicDataTable(self, musicData, musicDataAddresses):
self.rom.seek(self.musicDataTableAddress)
for dataFile in musicData:
addr = pc_to_snes(musicDataAddresses[dataFile]) if dataFile in musicDataAddresses else 0
self.rom.writeLong(addr)
def _getDataId(self, musicData, track):
return (musicData.index(self._nspc_path(self.allTracks[track]['nspc_path']))+1)*3
def _getTrackId(self, track):
return self.allTracks[track]['track_index'] + 5
def _updateReferences(self, trackList, musicData, replacedTracks):
trackAddresses = {}
def addAddresses(track, vanillaTrackData, prio=False):
nonlocal trackAddresses
addrs = []
prioAddrs = []
if 'pc_addresses' in vanillaTrackData:
addrs += vanillaTrackData['pc_addresses']
if self.area and 'pc_addresses_area' in vanillaTrackData:
prioAddrs += vanillaTrackData['pc_addresses_area']
if self.boss and 'pc_addresses_boss' in vanillaTrackData:
prioAddrs += vanillaTrackData['pc_addresses_boss']
if track not in trackAddresses:
trackAddresses[track] = []
# if prioAddrs are somewhere else, remove if necessary
prioSet = set(prioAddrs)
for t,tAddrs in trackAddresses.items():
trackAddresses[t] = list(set(tAddrs) - prioSet)
# if some of addrs are somewhere else, remove them from here
for t,tAddrs in trackAddresses.items():
addrs = list(set(addrs) - set(tAddrs))
trackAddresses[track] += prioAddrs + addrs
for track in trackList:
if track in replacedTracks.values():
for van,rep in replacedTracks.items():
if rep == track:
addAddresses(track, self.vanillaTracks[van])
else:
addAddresses(track, self.vanillaTracks[track])
for track in trackList:
dataId = self._getDataId(musicData, track)
trackId = self._getTrackId(track)
for addr in trackAddresses[track]:
self.rom.seek(addr)
self.rom.writeByte(dataId)
self.rom.writeByte(trackId)
self._writeSpecialReferences(replacedTracks, musicData)
# write special (boss) data
def _writeSpecialReferences(self, replacedTracks, musicData, static=True, dynamic=True):
for track,replacement in replacedTracks.items():
# static patches are needed only when replacing tracks
if track != replacement:
staticPatches = self.vanillaTracks[track].get("static_patches", None)
else:
staticPatches = None
# dynamic patches are similar to pc_addresses*, and must be written also
# when track is vanilla, as music data table is changed
dynamicPatches = self.vanillaTracks[track].get("dynamic_patches", None)
if static and staticPatches:
for addr,bytez in staticPatches.items():
self.rom.seek(int(addr))
for b in bytez:
self.rom.writeByte(b)
if dynamic and dynamicPatches:
dataId = self._getDataId(musicData, replacement)
trackId = self._getTrackId(replacement)
dataIdAddrs = dynamicPatches.get("data_id", [])
trackIdAddrs = dynamicPatches.get("track_id", [])
for addr in dataIdAddrs:
self.rom.writeByte(dataId, addr)
for addr in trackIdAddrs:
self.rom.writeByte(trackId, addr)
def _dump(self, output, trackList, musicData, musicDataAddresses):
music={}
no=0
for md in musicData:
if md is None:
music["NoData_%d" % no] = None
no += 1
else:
tracks = []
h,t=os.path.split(md)
md=os.path.join(os.path.split(h)[1], t)
for track,trackData in self.allTracks.items():
if trackData['nspc_path'] == md:
tracks.append(track)
music[md] = tracks
musicSnesAddresses = {}
for nspc, addr in musicDataAddresses.items():
h,t=os.path.split(nspc)
nspc=os.path.join(os.path.split(h)[1], t)
musicSnesAddresses[nspc] = "$%06x" % pc_to_snes(addr)
dump = {
"track_list": sorted(trackList),
"music_data": music,
"music_data_addresses": musicSnesAddresses
}
with open(output, 'w') as f:
json.dump(dump, f, indent=4)

View File

@ -1,224 +0,0 @@
#!/usr/bin/python3
import sys, argparse
from solver.interactiveSolver import InteractiveSolver
from solver.standardSolver import StandardSolver
from solver.conf import Conf
import utils.log
def interactiveSolver(args):
# to init, requires interactive/romFileName/presetFileName/output parameters in standard/plando mode
# to init, requires interactive/presetFileName/output parameters in seedless mode
# to iterate, requires interactive/state/[loc]/[item]/action/output parameters in item scope
# to iterate, requires interactive/state/[startPoint]/[endPoint]/action/output parameters in area scope
if args.action == 'init':
# init
if args.mode != 'seedless' and args.romFileName == None:
print("Missing romFileName parameter for {} mode".format(args.mode))
sys.exit(1)
if args.presetFileName == None or args.output == None:
print("Missing preset or output parameter")
sys.exit(1)
solver = InteractiveSolver(args.output, args.logic)
solver.initialize(args.mode, args.romFileName, args.presetFileName, magic=args.raceMagic, fill=args.fill, startLocation=args.startLocation)
else:
# iterate
params = {}
if args.scope == 'common':
if args.action == "save":
params["lock"] = args.lock
params["escapeTimer"] = args.escapeTimer
elif args.action == "randomize":
params["minorQty"] = args.minorQty
params["energyQty"] = args.energyQty
params["forbiddenItems"] = args.forbiddenItems.split(',') if args.forbiddenItems is not None else []
elif args.scope == 'item':
if args.state == None or args.action == None or args.output == None:
print("Missing state/action/output parameter")
sys.exit(1)
if args.action in ["add", "replace"]:
if args.mode not in ['seedless', 'race', 'debug'] and args.loc == None:
print("Missing loc parameter when using action add for item")
sys.exit(1)
if args.mode == 'plando':
if args.item == None:
print("Missing item parameter when using action add in plando/suitless mode")
sys.exit(1)
params = {'loc': args.loc, 'item': args.item, 'hide': args.hide}
elif args.action == "remove":
if args.loc != None:
params = {'loc': args.loc}
elif args.item != None:
params = {'item': args.item}
else:
params = {'count': args.count}
elif args.action == "toggle":
params = {'item': args.item}
elif args.scope == 'area':
if args.state == None or args.action == None or args.output == None:
print("Missing state/action/output parameter")
sys.exit(1)
if args.action == "add":
if args.startPoint == None or args.endPoint == None:
print("Missing start or end point parameter when using action add for item")
sys.exit(1)
params = {'startPoint': args.startPoint, 'endPoint': args.endPoint}
if args.action == "remove" and args.startPoint != None:
params = {'startPoint': args.startPoint}
elif args.scope == 'door':
if args.state == None or args.action == None or args.output == None:
print("Missing state/action/output parameter")
sys.exit(1)
if args.action == "replace":
if args.doorName is None or args.newColor is None:
print("Missing doorName or newColor parameter when using action replace for door")
sys.exit(1)
params = {'doorName': args.doorName, 'newColor': args.newColor}
elif args.action == "toggle":
if args.doorName is None:
print("Missing doorName parameter when using action toggle for door")
sys.exit(1)
params = {'doorName': args.doorName}
elif args.scope == 'dump':
if args.action == "import":
if args.dump is None:
print("Missing dump parameter when import a dump")
params = {'dump': args.dump}
params["debug"] = args.mode == 'debug'
solver = InteractiveSolver(args.output, args.logic)
solver.iterate(args.state, args.scope, args.action, params)
def standardSolver(args):
if args.romFileName is None:
print("Parameter --romFileName mandatory when not in interactive mode")
sys.exit(1)
if args.difficultyTarget is None:
difficultyTarget = Conf.difficultyTarget
else:
difficultyTarget = args.difficultyTarget
if args.pickupStrategy is None:
pickupStrategy = Conf.itemsPickup
else:
pickupStrategy = args.pickupStrategy
# itemsForbidden is like that: [['Varia'], ['Reserve'], ['Gravity']], fix it
args.itemsForbidden = [item[0] for item in args.itemsForbidden]
solver = StandardSolver(args.romFileName, args.presetFileName, difficultyTarget,
pickupStrategy, args.itemsForbidden, type=args.type,
firstItemsLog=args.firstItemsLog, extStatsFilename=args.extStatsFilename,
extStatsStep=args.extStatsStep,
displayGeneratedPath=args.displayGeneratedPath,
outputFileName=args.output, magic=args.raceMagic,
checkDuplicateMajor=args.checkDuplicateMajor, vcr=args.vcr,
runtimeLimit_s=args.runtimeLimit_s)
solver.solveRom()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Random Metroid Solver")
parser.add_argument('--romFileName', '-r', help="the input rom", nargs='?',
default=None, dest="romFileName")
parser.add_argument('--preset', '-p', help="the preset file", nargs='?',
default=None, dest='presetFileName')
parser.add_argument('--difficultyTarget', '-t',
help="the difficulty target that the solver will aim for",
dest='difficultyTarget', nargs='?', default=None, type=int)
parser.add_argument('--pickupStrategy', '-s', help="Pickup strategy for the Solver",
dest='pickupStrategy', nargs='?', default=None,
choices=['all', 'any'])
parser.add_argument('--itemsForbidden', '-f', help="Item not picked up during solving",
dest='itemsForbidden', nargs='+', default=[], action='append')
parser.add_argument('--type', '-y', help="web or console", dest='type', nargs='?',
default='console', choices=['web', 'console'])
parser.add_argument('--checkDuplicateMajor', dest="checkDuplicateMajor", action='store_true',
help="print a warning if the same major is collected more than once")
parser.add_argument('--debug', '-d', help="activate debug logging", dest='debug', action='store_true')
parser.add_argument('--firstItemsLog', '-1',
help="path to file where for each item type the first time it was found and where will be written (spoilers!)",
nargs='?', default=None, type=str, dest='firstItemsLog')
parser.add_argument('--ext_stats', help="Generate extended stats",
nargs='?', default=None, dest='extStatsFilename')
parser.add_argument('--ext_stats_step', help="what extended stats to generate",
nargs='?', default=None, dest='extStatsStep', type=int)
parser.add_argument('--displayGeneratedPath', '-g', help="display the generated path (spoilers!)",
dest='displayGeneratedPath', action='store_true')
parser.add_argument('--race', help="Race mode magic number", dest='raceMagic', type=int)
parser.add_argument('--vcr', help="Generate VCR output file", dest='vcr', action='store_true')
# standard/interactive, web site
parser.add_argument('--output', '-o', help="When called from the website, contains the result of the solver",
dest='output', nargs='?', default=None)
# interactive, web site
parser.add_argument('--interactive', '-i', help="Activate interactive mode for the solver",
dest='interactive', action='store_true')
parser.add_argument('--state', help="JSON file of the Solver state (used in interactive mode)",
dest="state", nargs='?', default=None)
parser.add_argument('--loc', help="Name of the location to action on (used in interactive mode)",
dest="loc", nargs='?', default=None)
parser.add_argument('--action', help="Pickup item at location, remove last pickedup location, clear all (used in interactive mode)",
dest="action", nargs="?", default=None, choices=['init', 'add', 'remove', 'clear', 'get', 'save', 'replace', 'randomize', 'toggle', 'import'])
parser.add_argument('--item', help="Name of the item to place in plando mode (used in interactive mode)",
dest="item", nargs='?', default=None)
parser.add_argument('--hide', help="Hide the item to place in plando mode (used in interactive mode)",
dest="hide", action='store_true')
parser.add_argument('--startPoint', help="The start AP to connect (used in interactive mode)",
dest="startPoint", nargs='?', default=None)
parser.add_argument('--endPoint', help="The destination AP to connect (used in interactive mode)",
dest="endPoint", nargs='?', default=None)
parser.add_argument('--mode', help="Solver mode: standard/seedless/plando (used in interactive mode)",
dest="mode", nargs="?", default=None, choices=['standard', 'seedless', 'plando', 'race', 'debug'])
parser.add_argument('--scope', help="Scope for the action: common/area/item (used in interactive mode)",
dest="scope", nargs="?", default=None, choices=['common', 'area', 'item', 'door', 'dump'])
parser.add_argument('--count', help="Number of item rollback (used in interactive mode)",
dest="count", type=int)
parser.add_argument('--lock', help="lock the plando seed (used in interactive mode)",
dest="lock", action='store_true')
parser.add_argument('--escapeTimer', help="escape timer like 03:00", dest="escapeTimer", default=None)
parser.add_argument('--fill', help="in plando load all the source seed locations/transitions as a base (used in interactive mode)",
dest="fill", action='store_true')
parser.add_argument('--startLocation', help="in plando/seedless: the start location", dest="startLocation", default="Landing Site")
parser.add_argument('--minorQty', help="rando plando (used in interactive mode)",
dest="minorQty", nargs="?", default=None, choices=[str(i) for i in range(0,101)])
parser.add_argument('--energyQty', help="rando plando (used in interactive mode)",
dest="energyQty", nargs="?", default=None, choices=["sparse", "medium", "vanilla"])
parser.add_argument('--forbiddenItems', help="rando plando (used in interactive mode)",
dest="forbiddenItems", nargs="?", default=None)
parser.add_argument('--doorName', help="door to replace (used in interactive mode)",
dest="doorName", nargs="?", default=None)
parser.add_argument('--newColor', help="new color for door (used in interactive mode)",
dest="newColor", nargs="?", default=None)
parser.add_argument('--logic', help='logic to use (used in interactive mode)', dest='logic', nargs='?', default="vanilla", choices=["vanilla", "rotation"])
parser.add_argument('--runtime',
help="Maximum runtime limit in seconds. If 0 or negative, no runtime limit.",
dest='runtimeLimit_s', nargs='?', default=0, type=int)
parser.add_argument('--dump', help="dump file with autotracker state (used in interactive mode)",
dest="dump", nargs="?", default=None)
args = parser.parse_args()
if args.presetFileName is None:
args.presetFileName = 'worlds/sm/variaRandomizer/standard_presets/regular.json'
if args.raceMagic != None:
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
print("Invalid magic")
sys.exit(-1)
if args.count != None:
if args.count < 1 or args.count > 0x80:
print("Invalid count")
sys.exit(-1)
utils.log.init(args.debug)
if args.interactive == True:
interactiveSolver(args)
else:
standardSolver(args)

View File

@ -0,0 +1 @@
{"Knows": {"WallJump": [true, 1], "ShineSpark": [true, 1], "MidAirMorph": [true, 1], "CrouchJump": [true, 1], "UnequipItem": [true, 1], "Mockball": [true, 1], "SimpleShortCharge": [true, 1], "InfiniteBombJump": [true, 1], "GreenGateGlitch": [true, 1], "ShortCharge": [false, 0], "GravityJump": [true, 1], "SpringBallJump": [true, 10], "SpringBallJumpFromWall": [true, 10], "GetAroundWallJump": [true, 1], "DraygonGrappleKill": [true, 1], "DraygonSparkKill": [false, 0], "MicrowaveDraygon": [true, 1], "MicrowavePhantoon": [true, 1], "LowAmmoCroc": [false, 0], "LowStuffBotwoon": [false, 0], "LowStuffGT": [false, 0], "IceZebSkip": [false, 0], "SpeedZebSkip": [false, 0], "HiJumpMamaTurtle": [false, 0], "MaridiaWallJumps": [false, 0], "MtEverestGravJump": [false, 0], "GravLessLevel1": [true, 10], "GravLessLevel2": [false, 0], "GravLessLevel3": [false, 0], "CeilingDBoost": [true, 1], "BillyMays": [true, 1], "AlcatrazEscape": [true, 1], "ReverseGateGlitch": [true, 1], "ReverseGateGlitchHiJumpLess": [true, 1], "EarlyKraid": [true, 1], "XrayDboost": [false, 0], "XrayIce": [false, 0], "RedTowerClimb": [true, 1], "RonPopeilScrew": [false, 0], "OldMBWithSpeed": [false, 0], "Moondance": [false, 0], "HiJumpLessGauntletAccess": [true, 1], "HiJumpGauntletAccess": [true, 1], "LowGauntlet": [false, 0], "IceEscape": [false, 0], "WallJumpCathedralExit": [true, 1], "BubbleMountainWallJump": [true, 1], "DoubleChamberWallJump": [false, 0], "NovaBoost": [false, 0], "NorfairReserveDBoost": [false, 0], "CrocPBsDBoost": [false, 0], "CrocPBsIce": [false, 0], "IceMissileFromCroc": [false, 0], "FrogSpeedwayWithoutSpeed": [false, 0], "LavaDive": [true, 1], "LavaDiveNoHiJump": [false, 0], "WorstRoomIceCharge": [false, 0], "WorstRoomWallJump": [false, 0], "ScrewAttackExit": [false, 0], "ScrewAttackExitWithoutScrew": [false, 0], "FirefleasWalljump": [false, 0], "DodgeLowerNorfairEnemies": [false, 0], "ContinuousWallJump": [true, 10], "DiagonalBombJump": [false, 0], "MockballWs": [true, 10], "SpongeBathBombJump": [false, 0], "SpongeBathHiJump": [true, 1], "SpongeBathSpeed": [true, 1], "TediousMountEverest": [false, 0], "DoubleSpringBallJump": [false, 0], "BotwoonToDraygonWithIce": [false, 0], "WestSandHoleSuitlessWallJumps": [false, 0], "DraygonRoomGrappleExit": [false, 0], "DraygonRoomCrystalFlash": [false, 0], "PreciousRoomXRayExit": [false, 0], "PreciousRoomGravJumpExit": [false, 0], "MochtroidClip": [true, 1], "PuyoClip": [false, 0], "PuyoClipXRay": [false, 0], "SnailClip": [false, 0], "SuitlessPuyoClip": [false, 0], "CrystalFlashClip": [false, 0], "SuitlessCrystalFlashClip": [false, 0], "KillPlasmaPiratesWithSpark": [false, 0], "KillPlasmaPiratesWithCharge": [true, 10], "AccessSpringBallWithHiJump": [true, 1], "AccessSpringBallWithSpringBallBombJumps": [false, 0], "AccessSpringBallWithBombJumps": [false, 0], "AccessSpringBallWithSpringBallJump": [false, 0], "AccessSpringBallWithXRayClimb": [false, 0], "AccessSpringBallWithGravJump": [false, 0], "AccessSpringBallWithFlatley": [false, 0]}, "Settings": {"Ice": "Gimme energy", "MainUpperNorfair": "Gimme energy", "LowerNorfair": "Default", "Kraid": "Default", "Phantoon": "Default", "Draygon": "Default", "Ridley": "Default", "MotherBrain": "Default", "X-Ray": "I don't like spikes", "Gauntlet": "Default"}, "Controller": {"A": "Jump", "B": "Dash", "X": "Shoot", "Y": "Item Cancel", "L": "Angle Down", "R": "Angle Up", "Select": "Item Select", "Moonwalk": true}, "password": "3d7e88af5a2f11e552eb2f7dc747cb1cbc810649086020eef598913c38a0a08b", "score": 221}

View File

@ -1,4 +1,5 @@
import random
from enum import IntEnum,IntFlag
import copy
from ..logic.smbool import SMBool
from ..rom.rom_patches import RomPatches
@ -11,7 +12,7 @@ colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice']
# 1/15 chance to have the door set to grey
colorsListGrey = colorsList * 2 + ['grey']
class Facing:
class Facing(IntEnum):
Left = 0
Right = 1
Top = 2
@ -38,9 +39,48 @@ colors2plm = {
'ice': plmIce
}
# door color indicators PLMs (flashing on the other side of colored doors)
indicatorsDirection = {
Facing.Left: Facing.Right,
Facing.Right: Facing.Left,
Facing.Top: Facing.Bottom,
Facing.Bottom: Facing.Top
}
# door facing left - right - top - bottom
plmRedIndicator = [0xFBB0, 0xFBB6, 0xFBBC, 0xFBC2]
plmGreenIndicator = [0xFBC8, 0xFBCE, 0xFBD4, 0xFBDA]
plmYellowIndicator = [0xFBE0, 0xFBE6, 0xFBEC, 0xFBF2]
plmGreyIndicator = [0xFBF8, 0xFBFE, 0xFC04, 0xFC0A]
plmWaveIndicator = [0xF60B, 0xF611, 0xF617, 0xF61D]
plmSpazerIndicator = [0xF63B, 0xF641, 0xF647, 0xF64D]
plmPlasmaIndicator = [0xF623, 0xF629, 0xF62F, 0xF635]
plmIceIndicator = [0xF653, 0xF659, 0xF65F, 0xF665]
colors2plmIndicator = {
'red': plmRedIndicator,
'green': plmGreenIndicator,
'yellow': plmYellowIndicator,
'grey': plmGreyIndicator,
'wave': plmWaveIndicator,
'spazer': plmSpazerIndicator,
'plasma': plmPlasmaIndicator,
'ice': plmIceIndicator
}
class IndicatorFlag(IntFlag):
Standard = 1
AreaRando = 2
DoorRando = 4
# indicator always there
IndicatorAll = IndicatorFlag.Standard | IndicatorFlag.AreaRando | IndicatorFlag.DoorRando
# indicator there when not in area rando
IndicatorDoor = IndicatorFlag.Standard | IndicatorFlag.DoorRando
class Door(object):
__slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors')
def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None):
__slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors','indicator')
def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None,indicator=0):
self.name = name
self.address = address
self.vanillaColor = vanillaColor
@ -52,6 +92,7 @@ class Door(object):
self.id = id
# list of forbidden colors
self.forbiddenColors = forbiddenColors
self.indicator = indicator
def forceBlue(self):
# custom start location, area, patches can force doors to blue
@ -115,21 +156,21 @@ class Door(object):
def isRefillSave(self):
return self.address is None
def writeColor(self, rom):
def writeColor(self, rom, writeWordFunc):
if self.isBlue() or self.isRefillSave():
return
rom.writeWord(colors2plm[self.color][self.facing], self.address)
writeWordFunc(colors2plm[self.color][self.facing], self.address)
# also set plm args high byte to never opened, even during escape
if self.color == 'grey':
rom.writeByte(0x90, self.address+5)
def readColor(self, rom):
def readColor(self, rom, readWordFunc):
if self.forced or self.isRefillSave():
return
plm = rom.readWord(self.address)
plm = readWordFunc(self.address)
if plm in plmRed:
self.setColor('red')
elif plm in plmGreen:
@ -147,7 +188,15 @@ class Door(object):
elif plm in plmIce:
self.setColor('ice')
else:
raise Exception("Unknown color {} for {}".format(hex(plm), self.name))
# we can't read the color, handle as grey door (can happen in race protected seeds)
self.setColor('grey')
# gives the PLM ID for matching indicator door
def getIndicatorPLM(self, indicatorFlags):
ret = None
if (indicatorFlags & self.indicator) != 0 and self.color in colors2plmIndicator:
ret = colors2plmIndicator[self.color][indicatorsDirection[self.facing]]
return ret
# for tracker
def canHide(self):
@ -179,10 +228,10 @@ class DoorsManager():
doorsDict = {}
doors = {
# crateria
'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True),
'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll),
'LandingSiteTopRight': Door('LandingSiteTopRight', 0x07801e, 'yellow', Facing.Left),
'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True),
'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True),
'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True, indicator=IndicatorDoor),
'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True, indicator=IndicatorAll),
'FlywayRight': Door('FlywayRight', 0x78420, 'red', Facing.Left),
'GreenPiratesShaftBottomRight': Door('GreenPiratesShaftBottomRight', 0x78470, 'red', Facing.Left, canGrey=True),
'RedBrinstarElevatorTop': Door('RedBrinstarElevatorTop', 0x78256, 'yellow', Facing.Bottom),
@ -190,34 +239,34 @@ class DoorsManager():
# blue brinstar
'ConstructionZoneRight': Door('ConstructionZoneRight', 0x78784, 'red', Facing.Left),
# green brinstar
'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True),
'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True),
'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True, indicator=IndicatorFlag.DoorRando),
'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True, indicator=IndicatorDoor),
'MainShaftRight': Door('MainShaftRight', 0x784be, 'red', Facing.Left),
'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True),
'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True, indicator=IndicatorAll),
'EarlySupersRight': Door('EarlySupersRight', 0x78512, 'red', Facing.Left),
'EtecoonEnergyTankLeft': Door('EtecoonEnergyTankLeft', 0x787c8, 'green', Facing.Right),
# pink brinstar
'BigPinkTopRight': Door('BigPinkTopRight', 0x78626, 'red', Facing.Left),
'BigPinkRight': Door('BigPinkRight', 0x7861a, 'yellow', Facing.Left),
'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True),
'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll),
'BigPinkBottomLeft': Door('BigPinkBottomLeft', 0x7862c, 'red', Facing.Right),
# red brinstar
'RedTowerLeft': Door('RedTowerLeft', 0x78866, 'yellow', Facing.Right),
'RedBrinstarFirefleaLeft': Door('RedBrinstarFirefleaLeft', 0x7886e, 'red', Facing.Right),
'RedTowerElevatorTopLeft': Door('RedTowerElevatorTopLeft', 0x788aa, 'green', Facing.Right),
'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right),
'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right, indicator=IndicatorAll),
'RedTowerElevatorBottomLeft': Door('RedTowerElevatorBottomLeft', 0x788b6, 'green', Facing.Right),
'BelowSpazerTopRight': Door('BelowSpazerTopRight', 0x78966, 'green', Facing.Left),
# Wrecked ship
'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True),
'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True),
'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top),
'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll),
'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True, indicator=IndicatorDoor),
'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top, indicator=IndicatorFlag.AreaRando),
'ElectricDeathRoomTopLeft': Door('ElectricDeathRoomTopLeft', 0x7c32f, 'red', Facing.Right),
# Upper Norfair
'BusinessCenterTopLeft': Door('BusinessCenterTopLeft', 0x78b00, 'green', Facing.Right),
'BusinessCenterBottomLeft': Door('BusinessCenterBottomLeft', 0x78b0c, 'red', Facing.Right),
'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True),
'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left),
'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True, indicator=IndicatorAll),
'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left, indicator=IndicatorAll),
'BubbleMountainTopRight': Door('BubbleMountainTopRight', 0x78c60, 'green', Facing.Left),
'BubbleMountainTopLeft': Door('BubbleMountainTopLeft', 0x78c5a, 'green', Facing.Right),
'SpeedBoosterHallRight': Door('SpeedBoosterHallRight', 0x78c7a, 'red', Facing.Left),
@ -229,13 +278,13 @@ class DoorsManager():
'PostCrocomireUpperLeft': Door('PostCrocomireUpperLeft', 0x78bf4, 'red', Facing.Right),
'PostCrocomireShaftRight': Door('PostCrocomireShaftRight', 0x78c0c, 'red', Facing.Left),
# Lower Norfair
'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top),
'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow']),
'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top, indicator=IndicatorFlag.AreaRando),
'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow'], indicator=IndicatorFlag.AreaRando),
# Maridia
'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left),
'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left, indicator=IndicatorAll),
'FishTankRight': Door('FishTankRight', 0x7c475, 'red', Facing.Left),
'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left),
'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left),
'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left, indicator=IndicatorDoor),
'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left, indicator=IndicatorFlag.AreaRando),
'PlasmaSparkBottom': Door('PlasmaSparkBottom', 0x7c577, 'green', Facing.Top),
'OasisTop': Door('OasisTop', 0x7c5d3, 'green', Facing.Bottom),
# refill/save
@ -310,12 +359,12 @@ class DoorsManager():
# call from rom loader
@staticmethod
def loadDoorsColor(rom):
def loadDoorsColor(rom, readWordFunc):
# force to blue some doors depending on patches
DoorsManager.setDoorsColor()
# for each door store it's color
for door in DoorsManager.doors.values():
door.readColor(rom)
door.readColor(rom, readWordFunc)
DoorsManager.debugDoorsColor()
# tell that we have randomized doors
@ -342,13 +391,24 @@ class DoorsManager():
# call from rom patcher
@staticmethod
def writeDoorsColor(rom, doors, player):
def writeDoorsColor(rom, doors, player, readWordFunc):
for door in DoorsManager.doorsDict[player].values():
door.writeColor(rom)
door.writeColor(rom, readWordFunc)
# also set save/refill doors to blue
if door.id is not None:
doors.append(door.id)
# returns a dict {'DoorName': indicatorPlmType }
@staticmethod
def getIndicatorPLMs(player, indicatorFlags):
ret = {}
for doorName,door in DoorsManager.doorsDict[player].items():
plm = door.getIndicatorPLM(indicatorFlags)
if plm is not None:
ret[doorName] = plm
return ret
# call from web
@staticmethod
def getAddressesToRead():

View File

@ -0,0 +1,804 @@
import copy
import random
from ..rom.addresses import Addresses
from ..rom.rom import pc_to_snes
from ..logic.helpers import Bosses
from ..logic.smbool import SMBool
from ..logic.logic import Logic
from ..graph.location import locationsDict
from ..utils.parameters import Knows
from ..utils import log
import logging
LOG = log.get('Objectives')
class Synonyms(object):
killSynonyms = [
"defeat",
"massacre",
"slay",
"wipe out",
"erase",
"finish",
"destroy",
"wreck",
"smash",
"crush",
"end"
]
alreadyUsed = []
@staticmethod
def getVerb():
verb = random.choice(Synonyms.killSynonyms)
while verb in Synonyms.alreadyUsed:
verb = random.choice(Synonyms.killSynonyms)
Synonyms.alreadyUsed.append(verb)
return verb
class Goal(object):
def __init__(self, name, gtype, logicClearFunc, romClearFunc,
escapeAccessPoints=None, objCompletedFuncAPs=lambda ap: [ap],
exclusion=None, items=None, text=None, introText=None,
available=True, expandableList=None, category=None, area=None,
conflictFunc=None):
self.name = name
self.available = available
self.clearFunc = logicClearFunc
self.objCompletedFuncAPs = objCompletedFuncAPs
# SNES addr in bank A1, see objectives.asm
self.checkAddr = pc_to_snes(Addresses.getOne("objective[%s]" % romClearFunc)) & 0xffff
self.escapeAccessPoints = escapeAccessPoints
if self.escapeAccessPoints is None:
self.escapeAccessPoints = (1, [])
self.rank = -1
# possible values:
# - boss
# - miniboss
# - other
self.gtype = gtype
# example for kill three g4
# {
# "list": [list of objectives],
# "type: "boss",
# "limit": 2
# }
self.exclusion = exclusion
if self.exclusion is None:
self.exclusion = {"list": []}
self.items = items
if self.items is None:
self.items = []
self.text = name if text is None else text
self.introText = introText
self.useSynonym = text is not None
self.expandableList = expandableList
if self.expandableList is None:
self.expandableList = []
self.expandable = len(self.expandableList) > 0
self.category = category
self.area = area
self.conflictFunc = conflictFunc
# used by solver/isolver to know if a goal has been completed
self.completed = False
def setRank(self, rank):
self.rank = rank
def canClearGoal(self, smbm, ap=None):
# not all objectives require an ap (like limit objectives)
return self.clearFunc(smbm, ap)
def getText(self):
out = "{}. ".format(self.rank)
if self.useSynonym:
out += self.text.format(Synonyms.getVerb())
else:
out += self.text
assert len(out) <= 28, "Goal text '{}' is too long: {}, max 28".format(out, len(out))
if self.introText is not None:
self.introText = "%d. %s" % (self.rank, self.introText)
else:
self.introText = out
return out
def getIntroText(self):
assert self.introText is not None
return self.introText
def isLimit(self):
return "type" in self.exclusion
def __repr__(self):
return self.name
def getBossEscapeAccessPoint(boss):
return (1, [Bosses.accessPoints[boss]])
def getG4EscapeAccessPoints(n):
return (n, [Bosses.accessPoints[boss] for boss in Bosses.Golden4()])
def getMiniBossesEscapeAccessPoints(n):
return (n, [Bosses.accessPoints[boss] for boss in Bosses.miniBosses()])
def getAreaEscapeAccessPoints(area):
return (1, list({list(loc.AccessFrom.keys())[0] for loc in Logic.locations if loc.GraphArea == area}))
_goalsList = [
Goal("kill kraid", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Kraid'), "kraid_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("Kraid"),
exclusion={"list": ["kill all G4", "kill one G4"]},
items=["Kraid"],
text="{} kraid",
category="Bosses"),
Goal("kill phantoon", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Phantoon'), "phantoon_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("Phantoon"),
exclusion={"list": ["kill all G4", "kill one G4"]},
items=["Phantoon"],
text="{} phantoon",
category="Bosses"),
Goal("kill draygon", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Draygon'), "draygon_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("Draygon"),
exclusion={"list": ["kill all G4", "kill one G4"]},
items=["Draygon"],
text="{} draygon",
category="Bosses"),
Goal("kill ridley", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Ridley'), "ridley_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("Ridley"),
exclusion={"list": ["kill all G4", "kill one G4"]},
items=["Ridley"],
text="{} ridley",
category="Bosses"),
Goal("kill one G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 1), "boss_1_killed",
escapeAccessPoints=getG4EscapeAccessPoints(1),
exclusion={"list": ["kill kraid", "kill phantoon", "kill draygon", "kill ridley",
"kill all G4", "kill two G4", "kill three G4"],
"type": "boss",
"limit": 0},
text="{} one golden4",
category="Bosses"),
Goal("kill two G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 2), "boss_2_killed",
escapeAccessPoints=getG4EscapeAccessPoints(2),
exclusion={"list": ["kill all G4", "kill one G4", "kill three G4"],
"type": "boss",
"limit": 1},
text="{} two golden4",
category="Bosses"),
Goal("kill three G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 3), "boss_3_killed",
escapeAccessPoints=getG4EscapeAccessPoints(3),
exclusion={"list": ["kill all G4", "kill one G4", "kill two G4"],
"type": "boss",
"limit": 2},
text="{} three golden4",
category="Bosses"),
Goal("kill all G4", "other", lambda sm, ap: Bosses.allBossesDead(sm), "all_g4_dead",
escapeAccessPoints=getG4EscapeAccessPoints(4),
exclusion={"list": ["kill kraid", "kill phantoon", "kill draygon", "kill ridley", "kill one G4", "kill two G4", "kill three G4"]},
items=["Kraid", "Phantoon", "Draygon", "Ridley"],
text="{} all golden4",
expandableList=["kill kraid", "kill phantoon", "kill draygon", "kill ridley"],
category="Bosses"),
Goal("kill spore spawn", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'SporeSpawn'), "spore_spawn_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("SporeSpawn"),
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
items=["SporeSpawn"],
text="{} spore spawn",
category="Minibosses"),
Goal("kill botwoon", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'Botwoon'), "botwoon_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("Botwoon"),
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
items=["Botwoon"],
text="{} botwoon",
category="Minibosses"),
Goal("kill crocomire", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'Crocomire'), "crocomire_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("Crocomire"),
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
items=["Crocomire"],
text="{} crocomire",
category="Minibosses"),
Goal("kill golden torizo", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'GoldenTorizo'), "golden_torizo_is_dead",
escapeAccessPoints=getBossEscapeAccessPoint("GoldenTorizo"),
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
items=["GoldenTorizo"],
text="{} golden torizo",
category="Minibosses",
conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))),
Goal("kill one miniboss", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 1), "miniboss_1_killed",
escapeAccessPoints=getMiniBossesEscapeAccessPoints(1),
exclusion={"list": ["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo",
"kill all mini bosses", "kill two minibosses", "kill three minibosses"],
"type": "miniboss",
"limit": 0},
text="{} one miniboss",
category="Minibosses"),
Goal("kill two minibosses", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 2), "miniboss_2_killed",
escapeAccessPoints=getMiniBossesEscapeAccessPoints(2),
exclusion={"list": ["kill all mini bosses", "kill one miniboss", "kill three minibosses"],
"type": "miniboss",
"limit": 1},
text="{} two minibosses",
category="Minibosses"),
Goal("kill three minibosses", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 3), "miniboss_3_killed",
escapeAccessPoints=getMiniBossesEscapeAccessPoints(3),
exclusion={"list": ["kill all mini bosses", "kill one miniboss", "kill two minibosses"],
"type": "miniboss",
"limit": 2},
text="{} three minibosses",
category="Minibosses"),
Goal("kill all mini bosses", "other", lambda sm, ap: Bosses.allMiniBossesDead(sm), "all_mini_bosses_dead",
escapeAccessPoints=getMiniBossesEscapeAccessPoints(4),
exclusion={"list": ["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo",
"kill one miniboss", "kill two minibosses", "kill three minibosses"]},
items=["SporeSpawn", "Botwoon", "Crocomire", "GoldenTorizo"],
text="{} all mini bosses",
expandableList=["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo"],
category="Minibosses",
conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))),
# not available in AP
#Goal("finish scavenger hunt", "other", lambda sm, ap: SMBool(True), "scavenger_hunt_completed",
# exclusion={"list": []}, # will be auto-completed
# available=False),
Goal("nothing", "other", lambda sm, ap: Objectives.objDict[sm.player].canAccess(sm, ap, "Landing Site"), "nothing_objective",
escapeAccessPoints=(1, ["Landing Site"])), # with no objectives at all, escape auto triggers only in crateria
Goal("collect 25% items", "items", lambda sm, ap: SMBool(True), "collect_25_items",
exclusion={"list": ["collect 50% items", "collect 75% items", "collect 100% items"]},
category="Items",
introText="collect 25 percent of items"),
Goal("collect 50% items", "items", lambda sm, ap: SMBool(True), "collect_50_items",
exclusion={"list": ["collect 25% items", "collect 75% items", "collect 100% items"]},
category="Items",
introText="collect 50 percent of items"),
Goal("collect 75% items", "items", lambda sm, ap: SMBool(True), "collect_75_items",
exclusion={"list": ["collect 25% items", "collect 50% items", "collect 100% items"]},
category="Items",
introText="collect 75 percent of items"),
Goal("collect 100% items", "items", lambda sm, ap: SMBool(True), "collect_100_items",
exclusion={"list": ["collect 25% items", "collect 50% items", "collect 75% items", "collect all upgrades"]},
category="Items",
introText="collect all items"),
Goal("collect all upgrades", "items", lambda sm, ap: SMBool(True), "all_major_items",
category="Items"),
Goal("clear crateria", "items", lambda sm, ap: SMBool(True), "crateria_cleared",
category="Items",
area="Crateria"),
Goal("clear green brinstar", "items", lambda sm, ap: SMBool(True), "green_brin_cleared",
category="Items",
area="GreenPinkBrinstar"),
Goal("clear red brinstar", "items", lambda sm, ap: SMBool(True), "red_brin_cleared",
category="Items",
area="RedBrinstar"),
Goal("clear wrecked ship", "items", lambda sm, ap: SMBool(True), "ws_cleared",
category="Items",
area="WreckedShip"),
Goal("clear kraid's lair", "items", lambda sm, ap: SMBool(True), "kraid_cleared",
category="Items",
area="Kraid"),
Goal("clear upper norfair", "items", lambda sm, ap: SMBool(True), "upper_norfair_cleared",
category="Items",
area="Norfair"),
Goal("clear croc's lair", "items", lambda sm, ap: SMBool(True), "croc_cleared",
category="Items",
area="Crocomire"),
Goal("clear lower norfair", "items", lambda sm, ap: SMBool(True), "lower_norfair_cleared",
category="Items",
area="LowerNorfair"),
Goal("clear west maridia", "items", lambda sm, ap: SMBool(True), "west_maridia_cleared",
category="Items",
area="WestMaridia"),
Goal("clear east maridia", "items", lambda sm, ap: SMBool(True), "east_maridia_cleared",
category="Items",
area="EastMaridia"),
Goal("tickle the red fish", "other",
lambda sm, ap: sm.wand(sm.haveItem('Grapple'), Objectives.objDict[sm.player].canAccess(sm, ap, "Red Fish Room Bottom")),
"fish_tickled",
escapeAccessPoints=(1, ["Red Fish Room Bottom"]),
objCompletedFuncAPs=lambda ap: ["Red Fish Room Bottom"],
category="Memes"),
Goal("kill the orange geemer", "other",
lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Bowling"), # XXX this unnecessarily adds canPassBowling as requirement
sm.wor(sm.haveItem('Wave'), sm.canUsePowerBombs())),
"orange_geemer",
escapeAccessPoints=(1, ["Bowling"]),
objCompletedFuncAPs=lambda ap: ["Bowling"],
text="{} orange geemer",
category="Memes"),
Goal("kill shaktool", "other",
lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Oasis Bottom"),
sm.canTraverseSandPits(),
sm.canAccessShaktoolFromPantsRoom()),
"shak_dead",
escapeAccessPoints=(1, ["Oasis Bottom"]),
objCompletedFuncAPs=lambda ap: ["Oasis Bottom"],
text="{} shaktool",
category="Memes"),
Goal("activate chozo robots", "other", lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccessLocation(sm, ap, "Bomb"),
Objectives.objDict[sm.player].canAccessLocation(sm, ap, "Gravity Suit"),
sm.haveItem("GoldenTorizo"),
sm.canPassLowerNorfairChozo()), # graph access implied by GT loc
"all_chozo_robots",
category="Memes",
escapeAccessPoints=(3, ["Landing Site", "Screw Attack Bottom", "Bowling"]),
objCompletedFuncAPs=lambda ap: ["Landing Site", "Screw Attack Bottom", "Bowling"],
exclusion={"list": ["kill golden torizo"]},
conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))),
Goal("visit the animals", "other", lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Big Pink"), sm.haveItem("SpeedBooster"), # dachora
Objectives.objDict[sm.player].canAccess(sm, ap, "Etecoons Bottom")), # Etecoons
"visited_animals",
category="Memes",
escapeAccessPoints=(2, ["Big Pink", "Etecoons Bottom"]),
objCompletedFuncAPs=lambda ap: ["Big Pink", "Etecoons Bottom"]),
Goal("kill king cacatac", "other",
lambda sm, ap: Objectives.objDict[sm.player].canAccess(sm, ap, 'Bubble Mountain Top'),
"king_cac_dead",
category="Memes",
escapeAccessPoints=(1, ['Bubble Mountain Top']),
objCompletedFuncAPs=lambda ap: ['Bubble Mountain Top'])
]
_goals = {goal.name:goal for goal in _goalsList}
def completeGoalData():
# "nothing" is incompatible with everything
_goals["nothing"].exclusion["list"] = [goal.name for goal in _goalsList]
areaGoals = [goal.name for goal in _goalsList if goal.area is not None]
# if we need 100% items, don't require "clear area", as it covers those
_goals["collect 100% items"].exclusion["list"] += areaGoals[:]
# if we have scav hunt, don't require "clear area" (HUD behaviour incompatibility)
# not available in AP
#_goals["finish scavenger hunt"].exclusion["list"] += areaGoals[:]
# remove clear area goals if disabled tourian, as escape can trigger as soon as an area is cleared,
# even if ship is not currently reachable
for goal in areaGoals:
_goals[goal].exclusion['tourian'] = "Disabled"
completeGoalData()
class Objectives(object):
maxActiveGoals = 5
vanillaGoals = ["kill kraid", "kill phantoon", "kill draygon", "kill ridley"]
scavHuntGoal = ["finish scavenger hunt"]
objDict = {}
def __init__(self, player=0, tourianRequired=True, randoSettings=None):
self.player = player
self.activeGoals = []
self.nbActiveGoals = 0
self.totalItemsCount = 100
self.goals = copy.deepcopy(_goals)
self.graph = None
self._tourianRequired = tourianRequired
self.randoSettings = randoSettings
Objectives.objDict[player] = self
@property
def tourianRequired(self):
assert self._tourianRequired is not None
return self._tourianRequired
def resetGoals(self):
self.activeGoals = []
self.nbActiveGoals = 0
def conflict(self, newGoal):
if newGoal.exclusion.get('tourian') == "Disabled" and self.tourianRequired == False:
LOG.debug("new goal %s conflicts with disabled Tourian" % newGoal.name)
return True
LOG.debug("check if new goal {} conflicts with existing active goals".format(newGoal.name))
count = 0
for goal in self.activeGoals:
if newGoal.name in goal.exclusion["list"]:
LOG.debug("new goal {} in exclusion list of active goal {}".format(newGoal.name, goal.name))
return True
if goal.name in newGoal.exclusion["list"]:
LOG.debug("active goal {} in exclusion list of new goal {}".format(goal.name, newGoal.name))
return True
# count bosses/minibosses already active if new goal has a limit
if newGoal.exclusion.get("type") == goal.gtype:
count += 1
LOG.debug("new goal limit type: {} same as active goal {}. count: {}".format(newGoal.exclusion["type"], goal.name, count))
if count > newGoal.exclusion.get("limit", 0):
LOG.debug("new goal {} limit {} is lower than active goals of type: {}".format(newGoal.name, newGoal.exclusion["limit"], newGoal.exclusion["type"]))
return True
LOG.debug("no direct conflict detected for new goal {}".format(newGoal.name))
# if at least one active goal has a limit and new goal has the same type of one of the existing limit
# check that new goal doesn't exceed the limit
for goal in self.activeGoals:
goalExclusionType = goal.exclusion.get("type")
if goalExclusionType is not None and goalExclusionType == newGoal.gtype:
count = 0
for lgoal in self.activeGoals:
if lgoal.gtype == newGoal.gtype:
count += 1
# add new goal to the count
if count >= goal.exclusion["limit"]:
LOG.debug("new Goal {} would excess limit {} of active goal {}".format(newGoal.name, goal.exclusion["limit"], goal.name))
return True
LOG.debug("no backward conflict detected for new goal {}".format(newGoal.name))
if self.randoSettings is not None and newGoal.conflictFunc is not None:
if newGoal.conflictFunc(self.randoSettings, self.player):
LOG.debug("new Goal {} is conflicting with rando settings".format(newGoal.name))
return True
LOG.debug("no conflict with rando settings detected for new goal {}".format(newGoal.name))
return False
def addGoal(self, goalName, completed=False):
LOG.debug("addGoal: {}".format(goalName))
goal = self.goals[goalName]
if self.conflict(goal):
return
self.nbActiveGoals += 1
assert self.nbActiveGoals <= self.maxActiveGoals, "Too many active goals"
goal.setRank(self.nbActiveGoals)
goal.completed = completed
self.activeGoals.append(goal)
def removeGoal(self, goal):
self.nbActiveGoals -= 1
self.activeGoals.remove(goal)
def clearGoals(self):
self.nbActiveGoals = 0
self.activeGoals.clear()
def isGoalActive(self, goalName):
return self.goals[goalName] in self.activeGoals
# having graph as a global sucks but Objectives instances are all over the place,
# goals must access it, and it doesn't change often
def setGraph(self, graph, maxDiff):
self.graph = graph
self.maxDiff = maxDiff
for goalName, goal in self.goals.items():
if goal.area is not None:
goal.escapeAccessPoints = getAreaEscapeAccessPoints(goal.area)
def canAccess(self, sm, src, dst):
return SMBool(self.graph.canAccess(sm, src, dst, self.maxDiff))
def canAccessLocation(self, sm, ap, locName):
loc = locationsDict[locName]
availLocs = self.graph.getAvailableLocations([loc], sm, self.maxDiff, ap)
return SMBool(loc in availLocs)
def setVanilla(self):
for goal in self.vanillaGoals:
self.addGoal(goal)
def isVanilla(self):
# kill G4 and/or scav hunt
if len(self.activeGoals) == 1:
for goal in self.activeGoals:
if goal.name not in self.scavHuntGoal:
return False
return True
elif len(self.activeGoals) == 4:
for goal in self.activeGoals:
if goal.name not in self.vanillaGoals:
return False
return True
elif len(self.activeGoals) == 5:
for goal in self.activeGoals:
if goal.name not in self.vanillaGoals + self.scavHuntGoal:
return False
return True
else:
return False
def setScavengerHunt(self):
self.addGoal("finish scavenger hunt")
def updateScavengerEscapeAccess(self, ap):
assert self.isGoalActive("finish scavenger hunt")
(_, apList) = self.goals['finish scavenger hunt'].escapeAccessPoints
apList.append(ap)
def _replaceEscapeAccessPoints(self, goal, aps):
(_, apList) = self.goals[goal].escapeAccessPoints
apList.clear()
apList += aps
def updateItemPercentEscapeAccess(self, collectedLocsAccessPoints):
for pct in [25,50,75,100]:
goal = 'collect %d%% items' % pct
self._replaceEscapeAccessPoints(goal, collectedLocsAccessPoints)
# not exactly accurate, but player has all upgrades to escape
self._replaceEscapeAccessPoints("collect all upgrades", collectedLocsAccessPoints)
def setScavengerHuntFunc(self, scavClearFunc):
self.goals["finish scavenger hunt"].clearFunc = scavClearFunc
def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None):
def getPctFunc(pct, totalItemsCount):
def f(sm, ap):
nonlocal pct, totalItemsCount
return sm.hasItemsPercent(pct, totalItemsCount)
return f
for pct in [25,50,75,100]:
goal = 'collect %d%% items' % pct
self.goals[goal].clearFunc = getPctFunc(pct, totalItemsCount)
if allUpgradeTypes is not None:
self.goals["collect all upgrades"].clearFunc = lambda sm, ap: sm.haveItems(allUpgradeTypes)
def setAreaFuncs(self, funcsByArea):
goalsByArea = {goal.area:goal for goalName, goal in self.goals.items()}
for area, func in funcsByArea.items():
if area in goalsByArea:
goalsByArea[area].clearFunc = func
def setSolverMode(self, solver):
self.setScavengerHuntFunc(solver.scavengerHuntComplete)
# in rando we know the number of items after randomizing, so set the functions only for the solver
self.setItemPercentFuncs(allUpgradeTypes=solver.majorUpgrades)
def getObjAreaFunc(area):
def f(sm, ap):
nonlocal solver, area
visitedLocs = set([loc.Name for loc in solver.visitedLocations])
return SMBool(all(locName in visitedLocs for locName in solver.splitLocsByArea[area]))
return f
self.setAreaFuncs({area:getObjAreaFunc(area) for area in solver.splitLocsByArea})
def expandGoals(self):
LOG.debug("Active goals:"+str(self.activeGoals))
# try to replace 'kill all G4' with the four associated objectives.
# we need at least 3 empty objectives out of the max (-1 +4)
if self.maxActiveGoals - self.nbActiveGoals < 3:
return
expandable = None
for goal in self.activeGoals:
if goal.expandable:
expandable = goal
break
if expandable is None:
return
LOG.debug("replace {} with {}".format(expandable.name, expandable.expandableList))
self.removeGoal(expandable)
for name in expandable.expandableList:
self.addGoal(name)
# rebuild ranks
for i, goal in enumerate(self.activeGoals, 1):
goal.rank = i
# call from logic
def canClearGoals(self, smbm, ap):
result = SMBool(True)
for goal in self.activeGoals:
result = smbm.wand(result, goal.canClearGoal(smbm, ap))
return result
# call from solver
def checkGoals(self, smbm, ap):
ret = {}
for goal in self.activeGoals:
if goal.completed is True:
continue
# check if goal can be completed
ret[goal.name] = goal.canClearGoal(smbm, ap)
return ret
def setGoalCompleted(self, goalName, completed):
for goal in self.activeGoals:
if goal.name == goalName:
goal.completed = completed
return
assert False, "Can't set goal {} completion to {}, goal not active".format(goalName, completed)
def allGoalsCompleted(self):
for goal in self.activeGoals:
if goal.completed is False:
return False
return True
def getGoalFromCheckFunction(self, checkFunction):
for name, goal in self.goals.items():
if goal.checkAddr == checkFunction:
return goal
assert True, "Goal with check function {} not found".format(hex(checkFunction))
def getTotalItemsCount(self):
return self.totalItemsCount
# call from web
def getAddressesToRead(self):
terminator = 1
objectiveSize = 2
bytesToRead = (self.maxActiveGoals + terminator) * objectiveSize
return [Addresses.getOne('objectivesList')+i for i in range(0, bytesToRead+1)] + Addresses.getWeb('totalItems') + Addresses.getWeb("itemsMask") + Addresses.getWeb("beamsMask")
def getExclusions(self):
# to compute exclusions in the front end
return {goalName: goal.exclusion for goalName, goal in self.goals.items()}
def getObjectivesTypes(self):
# to compute exclusions in the front end
types = {'boss': [], 'miniboss': []}
for goalName, goal in self.goals.items():
if goal.gtype in types:
types[goal.gtype].append(goalName)
return types
def getObjectivesSort(self):
return list(self.goals.keys())
def getObjectivesCategories(self):
return {goal.name: goal.category for goal in self.goals.values() if goal.category is not None}
# call from rando check pool and solver
def getMandatoryBosses(self):
r = [goal.items for goal in self.activeGoals]
return [item for items in r for item in items]
def checkLimitObjectives(self, beatableBosses):
# check that there's enough bosses/minibosses for limit objectives
from ..logic.smboolmanager import SMBoolManager
smbm = SMBoolManager(self.player)
smbm.addItems(beatableBosses)
for goal in self.activeGoals:
if not goal.isLimit():
continue
if not goal.canClearGoal(smbm):
return False
return True
# call from solver
def getGoalsList(self):
return [goal.name for goal in self.activeGoals]
# call from interactivesolver
def getState(self):
return {goal.name: goal.completed for goal in self.activeGoals}
def setState(self, state):
for goalName, completed in state.items():
self.addGoal(goalName, completed)
def resetGoals(self):
for goal in self.activeGoals:
goal.completed = False
# call from rando
@staticmethod
def getAllGoals(removeNothing=False):
return [goal.name for goal in _goals.values() if goal.available and (not removeNothing or goal.name != "nothing")]
# call from rando
def setRandom(self, nbGoals, availableGoals):
while self.nbActiveGoals < nbGoals and availableGoals:
goalName = random.choice(availableGoals)
self.addGoal(goalName)
availableGoals.remove(goalName)
# call from solver
def readGoals(self, romReader):
self.resetGoals()
romReader.romFile.seek(Addresses.getOne('objectivesList'))
checkFunction = romReader.romFile.readWord()
while checkFunction != 0x0000:
goal = self.getGoalFromCheckFunction(checkFunction)
self.activeGoals.append(goal)
checkFunction = romReader.romFile.readWord()
# read number of available items for items % objectives
self.totalItemsCount = romReader.romFile.readByte(Addresses.getOne('totalItems'))
for goal in self.activeGoals:
LOG.debug("active goal: {}".format(goal.name))
self._tourianRequired = not romReader.patchPresent('Escape_Trigger')
LOG.debug("tourianRequired: {}".format(self.tourianRequired))
# call from rando
def writeGoals(self, romFile):
# write check functions
romFile.seek(Addresses.getOne('objectivesList'))
for goal in self.activeGoals:
romFile.writeWord(goal.checkAddr)
# list terminator
romFile.writeWord(0x0000)
# compute chars
char2tile = {
'.': 0x4A,
'?': 0x4B,
'!': 0x4C,
' ': 0x00,
'%': 0x02,
'*': 0x03,
'0': 0x04,
'a': 0x30,
}
for i in range(1, ord('z')-ord('a')+1):
char2tile[chr(ord('a')+i)] = char2tile['a']+i
for i in range(1, ord('9')-ord('0')+1):
char2tile[chr(ord('0')+i)] = char2tile['0']+i
# write text
tileSize = 2
lineLength = 32 * tileSize
firstChar = 3 * tileSize
# start at 8th line
baseAddr = Addresses.getOne('objectivesText') + lineLength * 8 + firstChar
# space between two lines of text
space = 3 if self.nbActiveGoals == 5 else 4
for i, goal in enumerate(self.activeGoals):
addr = baseAddr + i * lineLength * space
text = goal.getText()
romFile.seek(addr)
for c in text:
if c not in char2tile:
continue
romFile.writeWord(0x3800 + char2tile[c])
# write goal completed positions y in sprites OAM
baseY = 0x40
addr = Addresses.getOne('objectivesSpritesOAM')
spritemapSize = 5 + 2
for i, goal in enumerate(self.activeGoals):
y = baseY + i * space * 8
# sprite center is at 128
y = (y - 128) & 0xFF
romFile.writeByte(y, addr+4 + i*spritemapSize)
def writeIntroObjectives(self, rom, tourian):
if self.isVanilla() and tourian == "Vanilla":
return
# objectives or tourian are not vanilla, prepare intro text
# two \n for an actual newline
text = "MISSION OBJECTIVES\n"
for goal in self.activeGoals:
text += "\n\n%s" % goal.getIntroText()
text += "\n\n\nTOURIAN IS %s\n\n\n" % tourian
text += "CHECK OBJECTIVES STATUS IN\n\n"
text += "THE PAUSE SCREEN"
# actually write text in ROM
self._writeIntroText(rom, text.upper())
def _writeIntroText(self, rom, text, startX=1, startY=2):
# for character translation
charCodes = {
' ': 0xD67D,
'.': 0xD75D,
'!': 0xD77B,
"'": 0xD76F,
'0': 0xD721,
'A': 0xD685
}
def addCharRange(start, end, base): # inclusive range
for c in range(ord(start), ord(end)+1):
offset = c - ord(base)
charCodes[chr(c)] = charCodes[base]+offset*6
addCharRange('B', 'Z', 'A')
addCharRange('1', '9', '0')
# actually write chars
x, y = startX, startY
def writeChar(c, frameDelay=2):
nonlocal rom, x, y
assert x <= 0x1F and y <= 0x18, "Intro text formatting error (x=0x%x, y=0x%x):\n%s" % (x, y, text)
if c == '\n':
x = startX
y += 1
else:
assert c in charCodes, "Invalid intro char "+c
rom.writeWord(frameDelay)
rom.writeByte(x)
rom.writeByte(y)
rom.writeWord(charCodes[c])
x += 1
rom.seek(Addresses.getOne('introText'))
for c in text:
writeChar(c)
# write trailer, see intro_text.asm
rom.writeWord(0xAE5B)
rom.writeWord(0x9698)

View File

@ -46,19 +46,19 @@ text2diff = {
def diff4solver(difficulty):
if difficulty == -1:
return "break"
return ("break", "break")
elif difficulty < medium:
return "easy"
return ("easy", "easy")
elif difficulty < hard:
return "medium"
return ("medium", "medium")
elif difficulty < harder:
return "hard"
return ("hard", "hard")
elif difficulty < hardcore:
return "harder"
return ("harder", "very hard")
elif difficulty < mania:
return "hardcore"
return ("hardcore", "hardcore")
else:
return "mania"
return ("mania", "mania")
# allow multiple local repo
appDir = str(Path(__file__).parents[4])
@ -120,7 +120,7 @@ class Knows:
Mockball = SMBool(True, easy, ['Mockball'])
desc['Mockball'] = {'display': 'Mockball',
'title': 'Morph from runing without loosing momentum to get Early Super and Ice Beam',
'title': 'Morph from running without loosing momentum to get Early Super and Ice Beam',
'href': 'https://wiki.supermetroid.run/index.php?title=Mockball',
'rooms': ['Early Supers Room', 'Ice Beam Gate Room']}
@ -161,7 +161,7 @@ class Knows:
SpringBallJump = SMBool(True, hard, ['SpringBallJump'])
desc['SpringBallJump'] = {'display': 'SpringBall-Jump',
'title': 'Do a SpringBall Jump from a jump to Access to Wrecked Ship Etank without anything else, Suitless Maridia navigation',
'href': 'https://www.youtube.com/watch?v=8ldQUIgBavw&t=49s',
'href': 'https://www.twitch.tv/videos/147442861',
'rooms': ['Sponge Bath', 'East Ocean',
'Main Street', 'Crab Shaft', 'Pseudo Plasma Spark Room',
'Mama Turtle Room', 'The Precious Room', 'Spring Ball Room', 'East Sand Hole',

View File

@ -56,7 +56,7 @@ def exists(resource: str):
return os.path.exists(resource)
def isStdPreset(preset):
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021']
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021', 'Torneio_SGPT3']
def getPresetDir(preset) -> str:
if isStdPreset(preset):
@ -316,6 +316,7 @@ class PresetLoaderDict(PresetLoader):
def getDefaultMultiValues():
from ..graph.graph_utils import GraphUtils
from ..utils.objectives import Objectives
defaultMultiValues = {
'startLocation': GraphUtils.getStartAccessPointNames(),
'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'],
@ -323,7 +324,10 @@ def getDefaultMultiValues():
'progressionDifficulty': ['easier', 'normal', 'harder'],
'morphPlacement': ['early', 'normal'], #['early', 'late', 'normal'],
'energyQty': ['ultra sparse', 'sparse', 'medium', 'vanilla'],
'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive']
'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive'],
'areaRandomization': ['off', 'full', 'light'],
'objective': Objectives.getAllGoals(removeNothing=True),
'tourian': ['Vanilla', 'Fast', 'Disabled']
}
return defaultMultiValues
@ -363,17 +367,16 @@ def loadRandoPreset(world, player, args):
args.noVariaTweaks = not world.varia_tweaks[player].value
args.maxDifficulty = diffs[world.max_difficulty[player].value]
#args.suitsRestriction = world.suits_restriction[player].value
#args.hideItems = world.hide_items[player].value
args.hideItems = world.hide_items[player].value
args.strictMinors = world.strict_minors[player].value
args.noLayout = not world.layout_patches[player].value
args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value]
args.nerfedCharge = world.nerfed_charge[player].value
args.area = world.area_randomization[player].value != 0
if args.area:
args.area = world.area_randomization[player].current_key
if args.area != "off":
args.areaLayoutBase = not world.area_layout[player].value
args.lightArea = world.area_randomization[player].value == 1
#args.escapeRando
#args.noRemoveEscapeEnemies
args.escapeRando = world.escape_rando[player].value
args.noRemoveEscapeEnemies = not world.remove_escape_enemies[player].value
args.doorsColorsRando = world.doors_colors_rando[player].value
args.allowGreyDoors = world.allow_grey_doors[player].value
args.bosses = world.boss_randomization[player].value
@ -384,7 +387,12 @@ def loadRandoPreset(world, player, args):
if world.fun_suits[player].value:
args.superFun.append("Suits")
ipsPatches = {"spin_jump_restart":"spinjumprestart", "rando_speed":"rando_speed", "elevators_doors_speed":"elevators_doors_speed", "refill_before_save":"refill_before_save"}
ipsPatches = { "spin_jump_restart":"spinjumprestart",
"rando_speed":"rando_speed",
"elevators_speed":"elevators_speed",
"fast_doors":"fast_doors",
"refill_before_save":"refill_before_save",
"relaxed_round_robin_cf":"relaxed_round_robin_cf"}
for settingName, patchName in ipsPatches.items():
if hasattr(world, settingName) and getattr(world, settingName)[player].value:
args.patches.append(patchName + '.ips')
@ -399,7 +407,6 @@ def loadRandoPreset(world, player, args):
#args.majorsSplit
#args.scavNumLocs
#args.scavRandomized
#args.scavEscape
args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value]
#args.progressionDifficulty
#args.progressionSpeed
@ -408,6 +415,8 @@ def loadRandoPreset(world, player, args):
args.powerBombQty = world.power_bomb_qty[player].value / float(10)
args.minorQty = world.minor_qty[player].value
args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value]
args.objective = world.objective[player].value
args.tourian = defaultMultiValues["tourian"][world.tourian[player].value]
#args.minimizerN
#args.minimizerTourian
@ -425,7 +434,6 @@ def getRandomizerDefaultParameters():
defaultParams['majorsSplitMultiSelect'] = defaultMultiValues['majorsSplit']
defaultParams['scavNumLocs'] = "10"
defaultParams['scavRandomized'] = "off"
defaultParams['scavEscape'] = "off"
defaultParams['startLocation'] = "Landing Site"
defaultParams['startLocationMultiSelect'] = defaultMultiValues['startLocation']
defaultParams['maxDifficulty'] = 'hardcore'
@ -444,9 +452,13 @@ def getRandomizerDefaultParameters():
defaultParams['minorQty'] = "100"
defaultParams['energyQty'] = "vanilla"
defaultParams['energyQtyMultiSelect'] = defaultMultiValues['energyQty']
defaultParams['objectiveRandom'] = "off"
defaultParams['nbObjective'] = "4"
defaultParams['objective'] = ["kill all G4"]
defaultParams['objectiveMultiSelect'] = defaultMultiValues['objective']
defaultParams['tourian'] = "Vanilla"
defaultParams['areaRandomization'] = "off"
defaultParams['areaLayout'] = "off"
defaultParams['lightAreaRandomization'] = "off"
defaultParams['doorsColorsRando'] = "off"
defaultParams['allowGreyDoors'] = "off"
defaultParams['escapeRando'] = "off"
@ -454,7 +466,6 @@ def getRandomizerDefaultParameters():
defaultParams['bossRandomization'] = "off"
defaultParams['minimizer'] = "off"
defaultParams['minimizerQty'] = "45"
defaultParams['minimizerTourian'] = "off"
defaultParams['funCombat'] = "off"
defaultParams['funMovement'] = "off"
defaultParams['funSuits'] = "off"
@ -463,8 +474,10 @@ def getRandomizerDefaultParameters():
defaultParams['gravityBehaviour'] = "Balanced"
defaultParams['gravityBehaviourMultiSelect'] = defaultMultiValues['gravityBehaviour']
defaultParams['nerfedCharge'] = "off"
defaultParams['relaxed_round_robin_cf'] = "off"
defaultParams['itemsounds'] = "on"
defaultParams['elevators_doors_speed'] = "on"
defaultParams['elevators_speed'] = "on"
defaultParams['fast_doors'] = "on"
defaultParams['spinjumprestart'] = "off"
defaultParams['rando_speed'] = "off"
defaultParams['Infinite_Space_Jump'] = "off"
@ -496,4 +509,22 @@ def fixEnergy(items):
items.append('{}-ETank'.format(maxETank))
if maxReserve > 0:
items.append('{}-Reserve'.format(maxReserve))
# keep biggest crystal flash
cfs = [i for i in items if i.find('CrystalFlash') != -1]
if len(cfs) > 1:
maxCf = 0
for cf in cfs:
nCf = int(cf[0:cf.find('-CrystalFlash')])
if nCf > maxCf:
maxCf = nCf
items.remove(cf)
items.append('{}-CrystalFlash'.format(maxCf))
return items
def dumpErrorMsg(outFileName, msg):
print("DIAG: " + msg)
if outFileName is not None:
with open(outFileName, 'w') as jsonFile:
json.dump({"errorMsg": msg}, jsonFile)

View File

@ -1,3 +1,5 @@
import Utils
# version displayed on the title screen, must be a max 32 chars [a-z0-9.-] string
# either 'beta' or 'r.yyyy.mm.dd'
displayedVersion = 'beta'
displayedVersion = 'r.2022.11.01-ap.' + Utils.__version__