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:
parent
0bc5a3bc8d
commit
6059b5ef66
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)":
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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))
|
||||
|
|
|
@ -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]
|
|
@ -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)
|
|
@ -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 = {}
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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}
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
|
@ -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__
|
||||
|
|
Loading…
Reference in New Issue