Adventure: implement new game (#1531)
Adds Adventure for the Atari 2600, NTSC version. New randomizer, not based on prior works. Somewhat atypical of current AP rom patch games; The generator does not require the adventure rom, but writes some data to an .apadvn APContainer file that the client uses along with a base bsdiff patch to generate a final rom file.
This commit is contained in:
53
worlds/adventure/Items.py
Normal file
53
worlds/adventure/Items.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import Optional
|
||||
from BaseClasses import ItemClassification, Item
|
||||
|
||||
base_adventure_item_id = 118000000
|
||||
|
||||
|
||||
class AdventureItem(Item):
|
||||
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
||||
super().__init__(name, classification, code, player)
|
||||
|
||||
|
||||
class ItemData:
|
||||
def __init__(self, id: int, classification: ItemClassification):
|
||||
self.classification = classification
|
||||
self.id = None if id is None else id + base_adventure_item_id
|
||||
self.table_index = id
|
||||
|
||||
|
||||
nothing_item_id = base_adventure_item_id
|
||||
|
||||
# base IDs are the index in the static item data table, which is
|
||||
# not the same order as the items in RAM (but offset 0 is a 16-bit address of
|
||||
# location of room and position data)
|
||||
item_table = {
|
||||
"Yellow Key": ItemData(0xB, ItemClassification.progression_skip_balancing),
|
||||
"White Key": ItemData(0xC, ItemClassification.progression),
|
||||
"Black Key": ItemData(0xD, ItemClassification.progression),
|
||||
"Bridge": ItemData(0xA, ItemClassification.progression),
|
||||
"Magnet": ItemData(0x11, ItemClassification.progression),
|
||||
"Sword": ItemData(0x9, ItemClassification.progression),
|
||||
"Chalice": ItemData(0x10, ItemClassification.progression_skip_balancing),
|
||||
# Non-ROM Adventure items, managed by lua
|
||||
"Left Difficulty Switch": ItemData(0x100, ItemClassification.filler),
|
||||
"Right Difficulty Switch": ItemData(0x101, ItemClassification.filler),
|
||||
# Can use these instead of 'nothing'
|
||||
"Freeincarnate": ItemData(0x102, ItemClassification.filler),
|
||||
# These should only be enabled if fast dragons is on?
|
||||
"Slow Yorgle": ItemData(0x103, ItemClassification.filler),
|
||||
"Slow Grundle": ItemData(0x104, ItemClassification.filler),
|
||||
"Slow Rhindle": ItemData(0x105, ItemClassification.filler),
|
||||
# this should only be enabled if opted into? For now, I'll just exclude them
|
||||
"Revive Dragons": ItemData(0x106, ItemClassification.trap),
|
||||
"nothing": ItemData(0x0, ItemClassification.filler)
|
||||
# Bat Trap
|
||||
# Bat Time Out
|
||||
# "Revive Dragons": ItemData(0x110, ItemClassification.trap)
|
||||
}
|
||||
|
||||
standard_item_max = item_table["Magnet"].id
|
||||
|
||||
|
||||
event_table = {
|
||||
}
|
||||
214
worlds/adventure/Locations.py
Normal file
214
worlds/adventure/Locations.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from BaseClasses import Location
|
||||
|
||||
base_location_id = 118000000
|
||||
|
||||
|
||||
class AdventureLocation(Location):
|
||||
game: str = "Adventure"
|
||||
|
||||
|
||||
class WorldPosition:
|
||||
room_id: int
|
||||
room_x: int
|
||||
room_y: int
|
||||
|
||||
def __init__(self, room_id: int, room_x: int = None, room_y: int = None):
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
|
||||
def get_position(self, random):
|
||||
if self.room_x is None or self.room_y is None:
|
||||
return random.choice(standard_positions)
|
||||
else:
|
||||
return self.room_x, self.room_y
|
||||
|
||||
|
||||
class LocationData:
|
||||
def __init__(self, region, name, location_id, world_positions: [WorldPosition] = None, event=False,
|
||||
needs_bat_logic: bool = False):
|
||||
self.region: str = region
|
||||
self.name: str = name
|
||||
self.world_positions: [WorldPosition] = world_positions
|
||||
self.room_id: int = None
|
||||
self.room_x: int = None
|
||||
self.room_y: int = None
|
||||
self.location_id: int = location_id
|
||||
if location_id is None:
|
||||
self.short_location_id: int = None
|
||||
self.location_id: int = None
|
||||
else:
|
||||
self.short_location_id: int = location_id
|
||||
self.location_id: int = location_id + base_location_id
|
||||
self.event: bool = event
|
||||
if world_positions is None and not event:
|
||||
self.room_id: int = self.short_location_id
|
||||
self.needs_bat_logic: int = needs_bat_logic
|
||||
self.local_item: int = None
|
||||
|
||||
def get_position(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
if self.room_id is None:
|
||||
return None
|
||||
self.room_x, self.room_y = random.choice(standard_positions)
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_x, self.room_y
|
||||
|
||||
def get_room_id(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
return None
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_id
|
||||
|
||||
|
||||
standard_positions = [
|
||||
(0x80, 0x20),
|
||||
(0x20, 0x20),
|
||||
(0x20, 0x40),
|
||||
(0x20, 0x40),
|
||||
(0x30, 0x20)
|
||||
]
|
||||
|
||||
|
||||
# Gives the most difficult region the dragon can reach and get stuck in from the provided room without the
|
||||
# player unlocking something for it
|
||||
def dragon_room_to_region(room: int) -> str:
|
||||
if room <= 0x11:
|
||||
return "Overworld"
|
||||
elif room <= 0x12:
|
||||
return "YellowCastle"
|
||||
elif room <= 0x16 or room == 0x1B:
|
||||
return "BlackCastle"
|
||||
elif room <= 0x1A:
|
||||
return "WhiteCastleVault"
|
||||
elif room <= 0x1D:
|
||||
return "Overworld"
|
||||
elif room <= 0x1E:
|
||||
return "CreditsRoom"
|
||||
|
||||
|
||||
def get_random_room_in_regions(regions: [str], random) -> int:
|
||||
possible_rooms = {}
|
||||
for locname in location_table:
|
||||
if location_table[locname].region in regions:
|
||||
room = location_table[locname].get_room_id(random)
|
||||
if room is not None:
|
||||
possible_rooms[room] = location_table[locname].room_id
|
||||
return random.choice(list(possible_rooms.keys()))
|
||||
|
||||
|
||||
location_table = {
|
||||
"Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4,
|
||||
[WorldPosition(0x4, 0x83, 0x47), # exit upper right
|
||||
WorldPosition(0x4, 0x12, 0x47), # exit upper left
|
||||
WorldPosition(0x4, 0x65, 0x20), # exit bottom right
|
||||
WorldPosition(0x4, 0x2A, 0x20), # exit bottom left
|
||||
WorldPosition(0x5, 0x4B, 0x60), # T room, top
|
||||
WorldPosition(0x5, 0x28, 0x1F), # T room, bottom left
|
||||
WorldPosition(0x5, 0x70, 0x1F), # T room, bottom right
|
||||
]),
|
||||
"Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x6,
|
||||
[WorldPosition(0x6, 0x8C, 0x20), # final turn bottom right
|
||||
WorldPosition(0x6, 0x03, 0x20), # final turn bottom left
|
||||
WorldPosition(0x6, 0x4B, 0x30), # final turn center
|
||||
WorldPosition(0x7, 0x4B, 0x40), # straightaway center
|
||||
WorldPosition(0x8, 0x40, 0x40), # entrance middle loop
|
||||
WorldPosition(0x8, 0x4B, 0x60), # entrance upper loop
|
||||
WorldPosition(0x8, 0x8C, 0x5E), # entrance right loop
|
||||
]),
|
||||
"Catacombs": LocationData("Overworld", "Catacombs", 0x9,
|
||||
[WorldPosition(0x9, 0x49, 0x40),
|
||||
WorldPosition(0x9, 0x4b, 0x20),
|
||||
WorldPosition(0xA),
|
||||
WorldPosition(0xA),
|
||||
WorldPosition(0xB, 0x40, 0x40),
|
||||
WorldPosition(0xB, 0x22, 0x1f),
|
||||
WorldPosition(0xB, 0x70, 0x1f)]),
|
||||
"Adjacent to Catacombs": LocationData("Overworld", "Adjacent to Catacombs", 0xC,
|
||||
[WorldPosition(0xC),
|
||||
WorldPosition(0xD)]),
|
||||
"Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE),
|
||||
"White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF),
|
||||
"Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10),
|
||||
"Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11),
|
||||
"Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12),
|
||||
"Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13,
|
||||
[WorldPosition(0x13),
|
||||
WorldPosition(0x14)]),
|
||||
"Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0xB5,
|
||||
[WorldPosition(0x15, 0x46, 0x1B)],
|
||||
needs_bat_logic=True),
|
||||
"Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x15,
|
||||
[WorldPosition(0x15),
|
||||
WorldPosition(0x16)]),
|
||||
"RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17,
|
||||
[WorldPosition(0x17, 0x70, 0x40), # right side third room
|
||||
WorldPosition(0x17, 0x18, 0x40), # left side third room
|
||||
WorldPosition(0x18, 0x20, 0x40),
|
||||
WorldPosition(0x18, 0x1A, 0x3F), # left side second room
|
||||
WorldPosition(0x18, 0x70, 0x3F), # right side second room
|
||||
]),
|
||||
"Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", 0xB7,
|
||||
[WorldPosition(0x17, 0x50, 0x60)],
|
||||
needs_bat_logic=True),
|
||||
"Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19,
|
||||
[WorldPosition(0x19, 0x4E, 0x35)],
|
||||
needs_bat_logic=True),
|
||||
"RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x1A), # entrance
|
||||
"Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B),
|
||||
"Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C),
|
||||
"Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D),
|
||||
"Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E,
|
||||
[WorldPosition(0x1E, 0x25, 0x50)]),
|
||||
"Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0xBE,
|
||||
[WorldPosition(0x1E, 0x70, 0x40)],
|
||||
needs_bat_logic=True),
|
||||
"Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True),
|
||||
"Slay Yorgle": LocationData("Varies", "Slay Yorgle", 0xD1, event=False),
|
||||
"Slay Grundle": LocationData("Varies", "Slay Grundle", 0xD2, event=False),
|
||||
"Slay Rhindle": LocationData("Varies", "Slay Rhindle", 0xD0, event=False),
|
||||
}
|
||||
|
||||
# the old location table, for reference
|
||||
location_table_old = {
|
||||
"Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4),
|
||||
"Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x5),
|
||||
"Blue Labyrinth 2": LocationData("Overworld", "Blue Labyrinth 2", 0x6),
|
||||
"Blue Labyrinth 3": LocationData("Overworld", "Blue Labyrinth 3", 0x7),
|
||||
"Blue Labyrinth 4": LocationData("Overworld", "Blue Labyrinth 4", 0x8),
|
||||
"Catacombs0": LocationData("Overworld", "Catacombs0", 0x9),
|
||||
"Catacombs1": LocationData("Overworld", "Catacombs1", 0xA),
|
||||
"Catacombs2": LocationData("Overworld", "Catacombs2", 0xB),
|
||||
"East of Catacombs": LocationData("Overworld", "East of Catacombs", 0xC),
|
||||
"West of Catacombs": LocationData("Overworld", "West of Catacombs", 0xD),
|
||||
"Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE),
|
||||
"White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF),
|
||||
"Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10),
|
||||
"Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11),
|
||||
"Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12),
|
||||
"Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13),
|
||||
"Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x14),
|
||||
"Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0x15,
|
||||
[WorldPosition(0xB5, 0x46, 0x1B)]),
|
||||
"Dungeon2": LocationData("BlackCastle", "Dungeon2", 0x15),
|
||||
"Dungeon3": LocationData("BlackCastle", "Dungeon3", 0x16),
|
||||
"RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, [WorldPosition(0x17, 0x70, 0x40)]),
|
||||
"RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x18, [WorldPosition(0x18, 0x20, 0x40)]),
|
||||
"Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance",
|
||||
0x17, [WorldPosition(0xB7, 0x50, 0x60)]),
|
||||
"Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, [WorldPosition(0x19, 0x4E, 0x35)]),
|
||||
"RedMaze3": LocationData("WhiteCastle", "RedMaze3", 0x1A),
|
||||
"Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B),
|
||||
"Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C),
|
||||
"Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D),
|
||||
"Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, [WorldPosition(0x1E, 0x25, 0x50)]),
|
||||
"Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0x1E,
|
||||
[WorldPosition(0xBE, 0x70, 0x40)]),
|
||||
"Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True)
|
||||
}
|
||||
46
worlds/adventure/Offsets.py
Normal file
46
worlds/adventure/Offsets.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# probably I should generate this from the list file
|
||||
|
||||
static_item_data_location = 0xe9d
|
||||
static_item_element_size = 9
|
||||
static_first_dragon_index = 6
|
||||
item_position_table = 0x402
|
||||
items_ram_start = 0xa1
|
||||
connector_port_offset = 0xff9
|
||||
# dragon speeds are hardcoded directly in their respective movement subroutines, not in their item table or state data
|
||||
# so this is the second byte of an LDA immediate instruction
|
||||
yorgle_speed_data_location = 0x724
|
||||
grundle_speed_data_location = 0x73f
|
||||
rhindle_speed_data_location = 0x709
|
||||
|
||||
|
||||
# in case I need to place a rom address in the rom
|
||||
rom_address_space_start = 0xf000
|
||||
|
||||
start_castle_offset = 0x39c
|
||||
start_castle_values = [0x11, 0x10, 0x0F]
|
||||
"""yellow, black, white castle gate rooms"""
|
||||
|
||||
# indexed by static item table index. 0x00 indicates the position data is in ROM and is irrelevant to the randomizer
|
||||
item_ram_addresses = [
|
||||
0xD9, # lamp
|
||||
0x00, # portcullis 1
|
||||
0x00, # portcullis 2
|
||||
0x00, # portcullis 3
|
||||
0x00, # author name
|
||||
0x00, # GO object
|
||||
0xA4, # Rhindle
|
||||
0xA9, # Yorgle
|
||||
0xAE, # Grundle
|
||||
0xB6, # Sword
|
||||
0xBC, # Bridge
|
||||
0xBF, # Yellow Key
|
||||
0xC2, # White key
|
||||
0xC5, # Black key
|
||||
0xCB, # Bat
|
||||
0xA1, # Dot
|
||||
0xB9, # Chalice
|
||||
0xB3, # Magnet
|
||||
0xE7, # AP object 1
|
||||
0xEA, # AP bat object
|
||||
0xBC, # NULL object (end of table)
|
||||
]
|
||||
244
worlds/adventure/Options.py
Normal file
244
worlds/adventure/Options.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
|
||||
|
||||
|
||||
class FreeincarnateMax(Range):
|
||||
"""How many maximum freeincarnate items to allow
|
||||
|
||||
When done generating items, any remaining item slots will be filled
|
||||
with freeincarnates, up to this maximum amount. Any remaining item
|
||||
slots after that will be 'nothing' items placed locally, so in multigame
|
||||
multiworlds, keeping this value high will allow more items from other games
|
||||
into Adventure.
|
||||
"""
|
||||
display_name = "Freeincarnate Maximum"
|
||||
range_start = 0
|
||||
range_end = 17
|
||||
default = 17
|
||||
|
||||
|
||||
class ItemRandoType(Choice):
|
||||
"""Choose how items are placed in the game
|
||||
|
||||
Not yet implemented. Currently only traditional supported
|
||||
Traditional: Adventure items are not in the map until
|
||||
they are collected (except local items) and are dropped
|
||||
on the player when collected. Adventure items are not checks.
|
||||
Inactive: Every item is placed, but is inactive until collected.
|
||||
Each item touched is a check. The bat ignores inactive items.
|
||||
|
||||
Supported values: traditional, inactive
|
||||
Default value: traditional
|
||||
"""
|
||||
|
||||
display_name = "Item type"
|
||||
option_traditional = 0x00
|
||||
option_inactive = 0x01
|
||||
default = option_traditional
|
||||
|
||||
|
||||
class DragonSlayCheck(DefaultOnToggle):
|
||||
"""If true, slaying each dragon for the first time is a check
|
||||
"""
|
||||
display_name = "Slay Dragon Checks"
|
||||
|
||||
|
||||
class TrapBatCheck(Choice):
|
||||
"""
|
||||
Locking the bat inside a castle may be a check
|
||||
|
||||
Not yet implemented
|
||||
If set to yes, the bat will not start inside a castle.
|
||||
Setting with_key requires the matching castle key to also be
|
||||
in the castle with the bat, achieved by dropping the key in the
|
||||
path of the portcullis as it falls. This setting is not recommended with the bat use_logic setting
|
||||
|
||||
Supported values: no, yes, with_key
|
||||
Default value: yes
|
||||
"""
|
||||
display_name = "Trap bat check"
|
||||
option_no_check = 0x0
|
||||
option_yes_key_optional = 0x1
|
||||
option_with_key = 0x2
|
||||
default = option_yes_key_optional
|
||||
|
||||
|
||||
class DragonRandoType(Choice):
|
||||
"""
|
||||
How to randomize the dragon starting locations
|
||||
|
||||
normal: Grundle is in the overworld, Yorgle in the white castle, and Rhindle in the black castle
|
||||
shuffle: A random dragon is placed in the overworld, one in the white castle, and one in the black castle
|
||||
overworldplus: Dragons can be placed anywhere, but at least one will be in the overworld
|
||||
randomized: Dragons can be anywhere except the credits room
|
||||
|
||||
|
||||
Supported values: normal, shuffle, overworldplus, randomized
|
||||
Default value: shuffle
|
||||
"""
|
||||
display_name = "Dragon Randomization"
|
||||
option_normal = 0x0
|
||||
option_shuffle = 0x1
|
||||
option_overworldplus = 0x2
|
||||
option_randomized = 0x3
|
||||
default = option_shuffle
|
||||
|
||||
|
||||
class BatLogic(Choice):
|
||||
"""How the bat is considered for logic
|
||||
|
||||
With cannot_break, the bat cannot pick up an item that starts out-of-logic until the player touches it
|
||||
With can_break, the bat is free to pick up any items, even if they are out-of-logic
|
||||
With use_logic, the bat can pick up anything just like can_break, and locations are no longer considered to require
|
||||
the magnet or bridge to collect, since the bat can retrieve these.
|
||||
A future option may allow the bat itself to be placed as an item.
|
||||
|
||||
Supported values: cannot_break, can_break, use_logic
|
||||
Default value: can_break
|
||||
"""
|
||||
display_name = "Bat Logic"
|
||||
option_cannot_break = 0x0
|
||||
option_can_break = 0x1
|
||||
option_use_logic = 0x2
|
||||
default = option_can_break
|
||||
|
||||
|
||||
class YorgleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Yorgle's initial speed. Yorgle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Yorgle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class YorgleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Yorgle's speed when all speed reducers are found. Yorgle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Yorgle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 1
|
||||
|
||||
|
||||
class GrundleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Grundle's initial speed. Grundle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Grundle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class GrundleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Grundle's speed when all speed reducers are found. Grundle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Grundle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 1
|
||||
|
||||
|
||||
class RhindleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Rhindle's initial speed. Rhindle has a speed of 3 in the original game
|
||||
Default value: 3
|
||||
"""
|
||||
display_name = "Rhindle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 3
|
||||
|
||||
|
||||
class RhindleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Rhindle's speed when all speed reducers are found. Rhindle has a speed of 3 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Rhindle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class ConnectorMultiSlot(Toggle):
|
||||
"""If true, the client and lua connector will add lowest 8 bits of the player slot
|
||||
to the port number used to connect to each other, to simplify connecting multiple local
|
||||
clients to local BizHawks.
|
||||
Set in the yaml, since the connector has to read this out of the rom file before connecting.
|
||||
"""
|
||||
display_name = "Connector Multi-Slot"
|
||||
|
||||
|
||||
class DifficultySwitchA(Choice):
|
||||
"""Set availability of left difficulty switch
|
||||
This controls the speed of the dragons' bite animation
|
||||
|
||||
"""
|
||||
display_name = "Left Difficulty Switch"
|
||||
option_normal = 0x0
|
||||
option_locked_hard = 0x1
|
||||
option_hard_with_unlock_item = 0x2
|
||||
default = option_hard_with_unlock_item
|
||||
|
||||
|
||||
class DifficultySwitchB(Choice):
|
||||
"""Set availability of right difficulty switch
|
||||
On hard, dragons will run away from the sword
|
||||
|
||||
"""
|
||||
display_name = "Right Difficulty Switch"
|
||||
option_normal = 0x0
|
||||
option_locked_hard = 0x1
|
||||
option_hard_with_unlock_item = 0x2
|
||||
default = option_hard_with_unlock_item
|
||||
|
||||
|
||||
class StartCastle(Choice):
|
||||
"""Choose or randomize which castle to start in front of.
|
||||
|
||||
This affects both normal start and reincarnation. Starting
|
||||
at the black castle may give easy dot runs, while starting
|
||||
at the white castle may make them more dangerous! Also, not
|
||||
starting at the yellow castle can make delivering the chalice
|
||||
with a full inventory slightly less trivial.
|
||||
|
||||
This doesn't affect logic since all the castles are reachable
|
||||
from each other.
|
||||
"""
|
||||
display_name = "Start Castle"
|
||||
option_yellow = 0
|
||||
option_black = 1
|
||||
option_white = 2
|
||||
default = option_yellow
|
||||
|
||||
|
||||
adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"dragon_slay_check": DragonSlayCheck,
|
||||
"death_link": DeathLink,
|
||||
"bat_logic": BatLogic,
|
||||
"freeincarnate_max": FreeincarnateMax,
|
||||
"dragon_rando_type": DragonRandoType,
|
||||
"connector_multi_slot": ConnectorMultiSlot,
|
||||
"yorgle_speed": YorgleStartingSpeed,
|
||||
"yorgle_min_speed": YorgleMinimumSpeed,
|
||||
"grundle_speed": GrundleStartingSpeed,
|
||||
"grundle_min_speed": GrundleMinimumSpeed,
|
||||
"rhindle_speed": RhindleStartingSpeed,
|
||||
"rhindle_min_speed": RhindleMinimumSpeed,
|
||||
"difficulty_switch_a": DifficultySwitchA,
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
||||
160
worlds/adventure/Regions.py
Normal file
160
worlds/adventure/Regions.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
|
||||
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||
one_way=False, name=None):
|
||||
source_region = world.get_region(source, player)
|
||||
target_region = world.get_region(target, player)
|
||||
|
||||
if name is None:
|
||||
name = source + " to " + target
|
||||
|
||||
connection = Entrance(
|
||||
player,
|
||||
name,
|
||||
source_region
|
||||
)
|
||||
|
||||
connection.access_rule = rule
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
if not one_way:
|
||||
connect(world, player, target, source, rule, True)
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
for name, locdata in location_table.items():
|
||||
locdata.get_position(multiworld.random)
|
||||
|
||||
menu = Region("Menu", player, multiworld)
|
||||
|
||||
menu.exits.append(Entrance(player, "GameStart", menu))
|
||||
multiworld.regions.append(menu)
|
||||
|
||||
overworld = Region("Overworld", player, multiworld)
|
||||
overworld.exits.append(Entrance(player, "YellowCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "WhiteCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "BlackCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "CreditsWall", overworld))
|
||||
multiworld.regions.append(overworld)
|
||||
|
||||
yellow_castle = Region("YellowCastle", player, multiworld, "Yellow Castle")
|
||||
yellow_castle.exits.append(Entrance(player, "YellowCastleExit", yellow_castle))
|
||||
multiworld.regions.append(yellow_castle)
|
||||
|
||||
white_castle = Region("WhiteCastle", player, multiworld, "White Castle")
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastleExit", white_castle))
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastleSecretPassage", white_castle))
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastlePeekPassage", white_castle))
|
||||
multiworld.regions.append(white_castle)
|
||||
|
||||
white_castle_pre_vault_peek = Region("WhiteCastlePreVaultPeek", player, multiworld, "White Castle Secret Peek")
|
||||
white_castle_pre_vault_peek.exits.append(Entrance(player, "WhiteCastleFromPeek", white_castle_pre_vault_peek))
|
||||
multiworld.regions.append(white_castle_pre_vault_peek)
|
||||
|
||||
white_castle_secret_room = Region("WhiteCastleVault", player, multiworld, "White Castle Vault",)
|
||||
white_castle_secret_room.exits.append(Entrance(player, "WhiteCastleReturnPassage", white_castle_secret_room))
|
||||
multiworld.regions.append(white_castle_secret_room)
|
||||
|
||||
black_castle = Region("BlackCastle", player, multiworld, "Black Castle")
|
||||
black_castle.exits.append(Entrance(player, "BlackCastleExit", black_castle))
|
||||
black_castle.exits.append(Entrance(player, "BlackCastleVaultEntrance", black_castle))
|
||||
multiworld.regions.append(black_castle)
|
||||
|
||||
black_castle_secret_room = Region("BlackCastleVault", player, multiworld, "Black Castle Vault")
|
||||
black_castle_secret_room.exits.append(Entrance(player, "BlackCastleReturnPassage", black_castle_secret_room))
|
||||
multiworld.regions.append(black_castle_secret_room)
|
||||
|
||||
credits_room = Region("CreditsRoom", player, multiworld, "Credits Room")
|
||||
credits_room.exits.append(Entrance(player, "CreditsExit", credits_room))
|
||||
credits_room.exits.append(Entrance(player, "CreditsToFarSide", credits_room))
|
||||
multiworld.regions.append(credits_room)
|
||||
|
||||
credits_room_far_side = Region("CreditsRoomFarSide", player, multiworld, "Credits Far Side")
|
||||
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
|
||||
multiworld.regions.append(credits_room_far_side)
|
||||
|
||||
dragon_slay_check = multiworld.dragon_slay_check[player].value
|
||||
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
|
||||
|
||||
for name, location_data in location_table.items():
|
||||
require_sword = False
|
||||
if location_data.region == "Varies":
|
||||
if location_data.name == "Slay Yorgle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[0])
|
||||
elif location_data.name == "Slay Grundle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[1])
|
||||
elif location_data.name == "Slay Rhindle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[2])
|
||||
else:
|
||||
raise Exception(f"Unknown location region for {location_data.name}")
|
||||
r = multiworld.get_region(region_name, player)
|
||||
else:
|
||||
r = multiworld.get_region(location_data.region, player)
|
||||
|
||||
adventure_loc = AdventureLocation(player, location_data.name, location_data.location_id, r)
|
||||
if adventure_loc.name in priority_locations:
|
||||
adventure_loc.progress_type = LocationProgressType.PRIORITY
|
||||
r.locations.append(adventure_loc)
|
||||
|
||||
# In a tracker and plando-free world, I'd determine unused locations here and not add them.
|
||||
# But that would cause problems with both plandos and trackers. So I guess I'll stick
|
||||
# with filling in with 'nothing' in pre_fill.
|
||||
|
||||
# in the future, I may randomize the map some, and that will require moving
|
||||
# connections to later, probably
|
||||
|
||||
multiworld.get_entrance("GameStart", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("YellowCastlePort", player) \
|
||||
.connect(multiworld.get_region("YellowCastle", player))
|
||||
multiworld.get_entrance("YellowCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("WhiteCastlePort", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
multiworld.get_entrance("WhiteCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("WhiteCastleSecretPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastleVault", player))
|
||||
multiworld.get_entrance("WhiteCastleReturnPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
multiworld.get_entrance("WhiteCastlePeekPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastlePreVaultPeek", player))
|
||||
multiworld.get_entrance("WhiteCastleFromPeek", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
|
||||
multiworld.get_entrance("BlackCastlePort", player) \
|
||||
.connect(multiworld.get_region("BlackCastle", player))
|
||||
multiworld.get_entrance("BlackCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
multiworld.get_entrance("BlackCastleVaultEntrance", player) \
|
||||
.connect(multiworld.get_region("BlackCastleVault", player))
|
||||
multiworld.get_entrance("BlackCastleReturnPassage", player) \
|
||||
.connect(multiworld.get_region("BlackCastle", player))
|
||||
|
||||
multiworld.get_entrance("CreditsWall", player) \
|
||||
.connect(multiworld.get_region("CreditsRoom", player))
|
||||
multiworld.get_entrance("CreditsExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("CreditsToFarSide", player) \
|
||||
.connect(multiworld.get_region("CreditsRoomFarSide", player))
|
||||
multiworld.get_entrance("CreditsFromFarSide", player) \
|
||||
.connect(multiworld.get_region("CreditsRoom", player))
|
||||
|
||||
|
||||
# Placeholder for adding sets of priority locations at generation, possibly as an option in the future
|
||||
def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
priority_locations = {}
|
||||
return priority_locations
|
||||
321
worlds/adventure/Rom.py
Normal file
321
worlds/adventure/Rom.py
Normal file
@@ -0,0 +1,321 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Optional, Any
|
||||
|
||||
import Utils
|
||||
from .Locations import AdventureLocation, LocationData
|
||||
from Utils import OptionsType
|
||||
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
|
||||
from itertools import chain
|
||||
|
||||
import bsdiff4
|
||||
|
||||
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"
|
||||
|
||||
|
||||
class AdventureAutoCollectLocation:
|
||||
short_location_id: int = 0
|
||||
room_id: int = 0
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
}
|
||||
|
||||
|
||||
class AdventureForeignItemInfo:
|
||||
short_location_id: int = 0
|
||||
room_id: int = 0
|
||||
room_x: int = 0
|
||||
room_y: int = 0
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
"room_x": self.room_x,
|
||||
"room_y": self.room_y,
|
||||
}
|
||||
|
||||
|
||||
class BatNoTouchLocation:
|
||||
short_location_id: int
|
||||
room_id: int
|
||||
room_x: int
|
||||
room_y: int
|
||||
local_item: int
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
self.local_item = local_item
|
||||
|
||||
def get_dict(self):
|
||||
ret_dict = {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
"room_x": self.room_x,
|
||||
"room_y": self.room_y,
|
||||
}
|
||||
if self.local_item is not None:
|
||||
ret_dict["local_item"] = self.local_item
|
||||
else:
|
||||
ret_dict["local_item"] = 255
|
||||
return ret_dict
|
||||
|
||||
|
||||
class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
hash = ADVENTUREHASH
|
||||
game = "Adventure"
|
||||
patch_file_ending = ".apadvn"
|
||||
zip_version: int = 2
|
||||
|
||||
# locations: [], autocollect: [], seed_name: bytes,
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
patch_only = True
|
||||
if "autocollect" in kwargs:
|
||||
patch_only = False
|
||||
self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y)
|
||||
for loc in kwargs["locations"]]
|
||||
|
||||
self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"]
|
||||
self.seedName: bytes = kwargs["seed_name"]
|
||||
self.local_item_locations: {} = kwargs["local_item_locations"]
|
||||
self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"]
|
||||
self.diff_a_mode: int = kwargs["diff_a_mode"]
|
||||
self.diff_b_mode: int = kwargs["diff_b_mode"]
|
||||
self.bat_logic: int = kwargs["bat_logic"]
|
||||
self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"]
|
||||
self.rom_deltas: {int, int} = kwargs["rom_deltas"]
|
||||
del kwargs["locations"]
|
||||
del kwargs["autocollect"]
|
||||
del kwargs["seed_name"]
|
||||
del kwargs["local_item_locations"]
|
||||
del kwargs["dragon_speed_reducer_info"]
|
||||
del kwargs["diff_a_mode"]
|
||||
del kwargs["diff_b_mode"]
|
||||
del kwargs["bat_logic"]
|
||||
del kwargs["bat_no_touch_locations"]
|
||||
del kwargs["rom_deltas"]
|
||||
super(AdventureDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("zip_version",
|
||||
self.zip_version.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.foreign_items is not None:
|
||||
loc_bytes = []
|
||||
for foreign_item in self.foreign_items:
|
||||
loc_bytes.append(foreign_item.short_location_id)
|
||||
loc_bytes.append(foreign_item.room_id)
|
||||
loc_bytes.append(foreign_item.room_x)
|
||||
loc_bytes.append(foreign_item.room_y)
|
||||
opened_zipfile.writestr("adventure_locations",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.autocollect_items is not None:
|
||||
loc_bytes = []
|
||||
for item in self.autocollect_items:
|
||||
loc_bytes.append(item.short_location_id)
|
||||
loc_bytes.append(item.room_id)
|
||||
opened_zipfile.writestr("adventure_autocollect",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.player_name is not None:
|
||||
opened_zipfile.writestr("player",
|
||||
self.player_name, # UTF-8
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.seedName is not None:
|
||||
opened_zipfile.writestr("seedName",
|
||||
self.seedName,
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.local_item_locations is not None:
|
||||
opened_zipfile.writestr("local_item_locations",
|
||||
json.dumps(self.local_item_locations),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.dragon_speed_reducer_info is not None:
|
||||
opened_zipfile.writestr("dragon_speed_reducer_info",
|
||||
json.dumps(self.dragon_speed_reducer_info),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.diff_a_mode is not None:
|
||||
opened_zipfile.writestr("diff_a_mode",
|
||||
self.diff_a_mode.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.diff_b_mode is not None:
|
||||
opened_zipfile.writestr("diff_b_mode",
|
||||
self.diff_b_mode.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.bat_logic is not None:
|
||||
opened_zipfile.writestr("bat_logic",
|
||||
self.bat_logic.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.bat_no_touch_locations is not None:
|
||||
loc_bytes = []
|
||||
for loc in self.bat_no_touch_locations:
|
||||
loc_bytes.append(loc.short_location_id) # used for AP items managed by script
|
||||
loc_bytes.append(loc.room_id) # used for local items placed in rom
|
||||
loc_bytes.append(loc.room_x)
|
||||
loc_bytes.append(loc.room_y)
|
||||
loc_bytes.append(0xff if loc.local_item is None else loc.local_item)
|
||||
opened_zipfile.writestr("bat_no_touch_locations",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.rom_deltas is not None:
|
||||
# this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter
|
||||
# if you're looking at doing something like this for another game, consider encoding your rom changes
|
||||
# in a more efficient way
|
||||
opened_zipfile.writestr("rom_deltas",
|
||||
json.dumps(self.rom_deltas),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile)
|
||||
self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile)
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
@classmethod
|
||||
def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool:
|
||||
version_bytes = opened_zipfile.read("zip_version")
|
||||
version = 0
|
||||
if version_bytes is not None:
|
||||
version = int.from_bytes(version_bytes, "little")
|
||||
if version != cls.zip_version:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str):
|
||||
seedbytes: bytes = opened_zipfile.read("seedName")
|
||||
namebytes: bytes = opened_zipfile.read("player")
|
||||
namestr: str = namebytes.decode("utf-8")
|
||||
return seedbytes, namestr
|
||||
|
||||
@classmethod
|
||||
def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int):
|
||||
diff_a_bytes = opened_zipfile.read("diff_a_mode")
|
||||
diff_b_bytes = opened_zipfile.read("diff_b_mode")
|
||||
diff_a = 0
|
||||
diff_b = 0
|
||||
if diff_a_bytes is not None:
|
||||
diff_a = int.from_bytes(diff_a_bytes, "little")
|
||||
if diff_b_bytes is not None:
|
||||
diff_b = int.from_bytes(diff_b_bytes, "little")
|
||||
return diff_a, diff_b
|
||||
|
||||
@classmethod
|
||||
def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int:
|
||||
bat_logic = opened_zipfile.read("bat_logic")
|
||||
if bat_logic is None:
|
||||
return 0
|
||||
return int.from_bytes(bat_logic, "little")
|
||||
|
||||
@classmethod
|
||||
def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
foreign_items = []
|
||||
readbytes: bytes = opened_zipfile.read("adventure_locations")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 4)):
|
||||
offset = i * 4
|
||||
foreign_items.append(AdventureForeignItemInfo(bytelist[offset],
|
||||
bytelist[offset + 1],
|
||||
bytelist[offset + 2],
|
||||
bytelist[offset + 3]))
|
||||
return foreign_items
|
||||
|
||||
@classmethod
|
||||
def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]:
|
||||
locations = []
|
||||
readbytes: bytes = opened_zipfile.read("bat_no_touch_locations")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 5)):
|
||||
offset = i * 5
|
||||
locations.append(BatNoTouchLocation(bytelist[offset],
|
||||
bytelist[offset + 1],
|
||||
bytelist[offset + 2],
|
||||
bytelist[offset + 3],
|
||||
bytelist[offset + 4]))
|
||||
return locations
|
||||
|
||||
@classmethod
|
||||
def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
autocollect_items = []
|
||||
readbytes: bytes = opened_zipfile.read("adventure_autocollect")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 2)):
|
||||
offset = i * 2
|
||||
autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1]))
|
||||
return autocollect_items
|
||||
|
||||
@classmethod
|
||||
def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
readbytes: bytes = opened_zipfile.read("local_item_locations")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}:
|
||||
readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}:
|
||||
readbytes: bytes = opened_zipfile.read("rom_deltas")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray:
|
||||
rom_bytes = bytearray(base_bytes)
|
||||
for offset, value in rom_deltas.items():
|
||||
int_offset = int(offset)
|
||||
rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little")
|
||||
return rom_bytes
|
||||
|
||||
|
||||
def apply_basepatch(base_rom_bytes: bytes) -> bytes:
|
||||
with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch:
|
||||
delta: bytes = basepatch.read()
|
||||
return bsdiff4.patch(base_rom_bytes, delta)
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
with open(file_name, "rb") as file:
|
||||
base_rom_bytes = bytes(file.read())
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if ADVENTUREHASH != basemd5.hexdigest():
|
||||
raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. "
|
||||
"Get the correct game and version, then dump it")
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: OptionsType = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["adventure_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
98
worlds/adventure/Rules.py
Normal file
98
worlds/adventure/Rules.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from worlds.adventure import location_table
|
||||
from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
from BaseClasses import LocationProgressType
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
world = self.multiworld
|
||||
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
|
||||
|
||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
||||
lambda state: state.has("Yellow Key", self.player))
|
||||
set_rule(world.get_entrance("BlackCastlePort", self.player),
|
||||
lambda state: state.has("Black Key", self.player))
|
||||
set_rule(world.get_entrance("WhiteCastlePort", self.player),
|
||||
lambda state: state.has("White Key", self.player))
|
||||
|
||||
# a future thing would be to make the bat an actual item, or at least allow it to
|
||||
# be placed in a castle, which would require some additions to the rules when
|
||||
# use_bat_logic is true
|
||||
if not use_bat_logic:
|
||||
set_rule(world.get_entrance("WhiteCastleSecretPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player))
|
||||
set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
set_rule(world.get_entrance("BlackCastleVaultEntrance", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
|
||||
dragon_slay_check = world.dragon_slay_check[self.player].value
|
||||
if dragon_slay_check:
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
set_rule(world.get_location("Slay Grundle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
else:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
set_rule(world.get_location("Slay Grundle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
|
||||
# really this requires getting the dot item, and having another item or enemy
|
||||
# in the room, but the dot would be *super evil*
|
||||
# to actually make randomized, since it is invisible. May add some options
|
||||
# for how that works in the distant future, but for now, just say you need
|
||||
# the bridge and black key to get to it, as that simplifies things a lot
|
||||
set_rule(world.get_entrance("CreditsWall", self.player),
|
||||
lambda state: state.has("Bridge", self.player) and
|
||||
state.has("Black Key", self.player))
|
||||
|
||||
if not use_bat_logic:
|
||||
set_rule(world.get_entrance("CreditsToFarSide", self.player),
|
||||
lambda state: state.has("Magnet", self.player))
|
||||
|
||||
# bridge literally does not fit in this space, I think. I'll just exclude it
|
||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Bridge", self.player)
|
||||
# don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play
|
||||
if not use_bat_logic:
|
||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Magnet", self.player)
|
||||
forbid_item(world.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player)
|
||||
forbid_item(world.get_location("Credits Right Side", self.player), "Magnet", self.player)
|
||||
|
||||
# and obviously we don't want to start with the game already won
|
||||
forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
||||
overworld = world.get_region("Overworld", self.player)
|
||||
|
||||
for loc in overworld.locations:
|
||||
forbid_item(loc, "Chalice", self.player)
|
||||
|
||||
add_rule(world.get_location("Chalice Home", self.player),
|
||||
lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player))
|
||||
|
||||
# world.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
# all_locations = world.get_locations(self.player).copy()
|
||||
# while priority_count < get_num_items():
|
||||
# loc = world.random.choice(all_locations)
|
||||
# if loc.progress_type == LocationProgressType.DEFAULT:
|
||||
# loc.progress_type = LocationProgressType.PRIORITY
|
||||
# priority_count += 1
|
||||
# all_locations.remove(loc)
|
||||
|
||||
# TODO: Add events for dragon_slay_check and trap_bat_check. Here? Elsewhere?
|
||||
# if self.dragon_slay_check == 1:
|
||||
# TODO - Randomize bat and dragon start rooms and use those to determine rules
|
||||
# TODO - for the requirements for the slay event (since we have to get to the
|
||||
# TODO - dragons and sword to kill them). Unless the dragons are set to be items,
|
||||
# TODO - which might be a funny option, then they can just be randoed like normal
|
||||
# TODO - just forbidden from the vaults and all credits room locations
|
||||
391
worlds/adventure/__init__.py
Normal file
391
worlds/adventure/__init__.py
Normal file
@@ -0,0 +1,391 @@
|
||||
import base64
|
||||
import copy
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \
|
||||
LocationProgressType
|
||||
from Main import __version__
|
||||
from Options import AssembleOptions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
|
||||
AdventureAutoCollectLocation
|
||||
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
|
||||
from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions
|
||||
from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \
|
||||
static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \
|
||||
rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
|
||||
|
||||
from worlds.LauncherComponents import Component, components, SuffixIdentifier
|
||||
|
||||
# Adventure
|
||||
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))
|
||||
|
||||
|
||||
class AdventureWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up Adventure for MultiWorld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["JusticePS"]
|
||||
)]
|
||||
theme = "dirt"
|
||||
|
||||
|
||||
def get_item_position_data_start(table_index: int):
|
||||
item_ram_address = item_ram_addresses[table_index];
|
||||
return item_position_table + item_ram_address - items_ram_start
|
||||
|
||||
|
||||
class AdventureWorld(World):
|
||||
"""
|
||||
Adventure for the Atari 2600 is an early graphical adventure game.
|
||||
Find the enchanted chalice and return it to the yellow castle,
|
||||
using magic items to enter hidden rooms, retrieve out of
|
||||
reach items, or defeat the three dragons. Beware the bat
|
||||
who likes to steal your equipment!
|
||||
"""
|
||||
game: ClassVar[str] = "Adventure"
|
||||
web: ClassVar[WebWorld] = AdventureWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
data_version: ClassVar[int] = 1
|
||||
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.rom_name: Optional[bytearray] = bytearray("", "utf8" )
|
||||
self.dragon_rooms: [int] = [0x14, 0x19, 0x4]
|
||||
self.dragon_slay_check: Optional[int] = 0
|
||||
self.connector_multi_slot: Optional[int] = 0
|
||||
self.dragon_rando_type: Optional[int] = 0
|
||||
self.yorgle_speed: Optional[int] = 2
|
||||
self.yorgle_min_speed: Optional[int] = 2
|
||||
self.grundle_speed: Optional[int] = 2
|
||||
self.grundle_min_speed: Optional[int] = 2
|
||||
self.rhindle_speed: Optional[int] = 3
|
||||
self.rhindle_min_speed: Optional[int] = 3
|
||||
self.difficulty_switch_a: Optional[int] = 0
|
||||
self.difficulty_switch_b: Optional[int] = 0
|
||||
self.start_castle: Optional[int] = 0
|
||||
# dict of item names -> list of speed deltas
|
||||
self.dragon_speed_reducer_info: {} = {}
|
||||
self.created_items: int = 0
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, _multiworld: MultiWorld) -> None:
|
||||
# don't need rom anymore
|
||||
pass
|
||||
|
||||
def place_random_dragon(self, dragon_index: int):
|
||||
region_list = ["Overworld", "YellowCastle", "BlackCastle", "WhiteCastle"]
|
||||
self.dragon_rooms[dragon_index] = get_random_room_in_regions(region_list, self.multiworld.random)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.rom_name = \
|
||||
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
|
||||
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
|
||||
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
|
||||
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
|
||||
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
|
||||
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
|
||||
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
|
||||
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
|
||||
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
|
||||
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
|
||||
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
|
||||
self.start_castle = self.multiworld.start_castle[self.player].value
|
||||
self.created_items = 0
|
||||
|
||||
if self.dragon_slay_check == 0:
|
||||
item_table["Sword"].classification = ItemClassification.useful
|
||||
else:
|
||||
item_table["Sword"].classification = ItemClassification.progression
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
item_table["Right Difficulty Switch"].classification = ItemClassification.progression
|
||||
|
||||
if self.dragon_rando_type == DragonRandoType.option_shuffle:
|
||||
self.multiworld.random.shuffle(self.dragon_rooms)
|
||||
elif self.dragon_rando_type == DragonRandoType.option_overworldplus:
|
||||
dragon_indices = [0, 1, 2]
|
||||
overworld_forced_index = self.multiworld.random.choice(dragon_indices)
|
||||
dragon_indices.remove(overworld_forced_index)
|
||||
region_list = ["Overworld"]
|
||||
self.dragon_rooms[overworld_forced_index] = get_random_room_in_regions(region_list, self.multiworld.random)
|
||||
self.place_random_dragon(dragon_indices[0])
|
||||
self.place_random_dragon(dragon_indices[1])
|
||||
elif self.dragon_rando_type == DragonRandoType.option_randomized:
|
||||
self.place_random_dragon(0)
|
||||
self.place_random_dragon(1)
|
||||
self.place_random_dragon(2)
|
||||
|
||||
def create_items(self) -> None:
|
||||
for event in map(self.create_item, event_table):
|
||||
self.multiworld.itempool.append(event)
|
||||
exclude = [item for item in self.multiworld.precollected_items[self.player]]
|
||||
self.created_items = 0
|
||||
for item in map(self.create_item, item_table):
|
||||
if item.code == nothing_item_id:
|
||||
continue
|
||||
if item in exclude and item.code <= standard_item_max:
|
||||
exclude.remove(item) # this is destructive. create unique list above
|
||||
else:
|
||||
if item.code <= standard_item_max:
|
||||
self.multiworld.itempool.append(item)
|
||||
self.created_items += 1
|
||||
num_locations = len(location_table) - 1 # subtract out the chalice location
|
||||
if self.dragon_slay_check == 0:
|
||||
num_locations -= 3
|
||||
|
||||
if self.difficulty_switch_a == DifficultySwitchA.option_hard_with_unlock_item:
|
||||
self.multiworld.itempool.append(self.create_item("Left Difficulty Switch"))
|
||||
self.created_items += 1
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
self.multiworld.itempool.append(self.create_item("Right Difficulty Switch"))
|
||||
self.created_items += 1
|
||||
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
self.dragon_speed_reducer_info = {}
|
||||
# make sure yorgle doesn't take 2 if there's not enough for the others to get at least one
|
||||
if extra_filler_count <= 4:
|
||||
extra_filler_count = 1
|
||||
self.create_dragon_slow_items(self.yorgle_min_speed, self.yorgle_speed, "Slow Yorgle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
if extra_filler_count <= 3:
|
||||
extra_filler_count = 1
|
||||
self.create_dragon_slow_items(self.grundle_min_speed, self.grundle_speed, "Slow Grundle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
self.create_dragon_slow_items(self.rhindle_min_speed, self.rhindle_speed, "Slow Rhindle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
# traps would probably go here, if enabled
|
||||
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
|
||||
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
|
||||
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
|
||||
self.created_items += actual_freeincarnates
|
||||
|
||||
def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, maximum_items: int):
|
||||
if min_speed < speed:
|
||||
delta = speed - min_speed
|
||||
if delta > 2 and maximum_items >= 2:
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
speed_with_one = speed - math.floor(delta / 2)
|
||||
self.dragon_speed_reducer_info[item_table[item_name].id] = [speed_with_one, min_speed]
|
||||
self.created_items += 2
|
||||
elif maximum_items >= 1:
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
self.dragon_speed_reducer_info[item_table[item_name].id] = [min_speed]
|
||||
self.created_items += 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||
self.create_event("Victory", ItemClassification.progression))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def pre_fill(self):
|
||||
# Place empty items in filler locations here, to limit
|
||||
# the number of exported empty items and the density of stuff in overworld.
|
||||
max_location_count = len(location_table) - 1
|
||||
if self.dragon_slay_check == 0:
|
||||
max_location_count -= 3
|
||||
|
||||
force_empty_item_count = (max_location_count - self.created_items)
|
||||
if force_empty_item_count <= 0:
|
||||
return
|
||||
overworld = self.multiworld.get_region("Overworld", self.player)
|
||||
overworld_locations_copy = overworld.locations.copy()
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
|
||||
locations_copy = all_locations.copy()
|
||||
for loc in all_locations:
|
||||
if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT:
|
||||
locations_copy.remove(loc)
|
||||
if loc in overworld_locations_copy:
|
||||
overworld_locations_copy.remove(loc)
|
||||
|
||||
# guarantee at least one overworld location, so we can for sure get a key somewhere
|
||||
# if too much stuff is plando'd though, just let it go
|
||||
if len(overworld_locations_copy) >= 3:
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# if we have few items, enforce another overworld slot, fill a hard slot, and ensure we have
|
||||
# at least one hard slot available
|
||||
if self.created_items < 15:
|
||||
hard_locations = []
|
||||
for loc in locations_copy:
|
||||
if "Vault" in loc.name or "Credits" in loc.name:
|
||||
hard_locations.append(loc)
|
||||
force_empty_item_count -= 1
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
hard_locations.remove(loc)
|
||||
locations_copy.remove(loc)
|
||||
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
locations_copy.remove(loc)
|
||||
hard_locations.remove(loc)
|
||||
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# if we have very few items, fill another two difficult slots
|
||||
if self.created_items < 10:
|
||||
for i in range(2):
|
||||
force_empty_item_count -= 1
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
hard_locations.remove(loc)
|
||||
locations_copy.remove(loc)
|
||||
|
||||
# for the absolute minimum number of items, enforce a third overworld slot
|
||||
if self.created_items <= 7:
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# finally, place nothing items
|
||||
while force_empty_item_count > 0 and locations_copy:
|
||||
force_empty_item_count -= 1
|
||||
# prefer somewhat to thin out the overworld.
|
||||
if len(overworld_locations_copy) > 0 and self.multiworld.random.randint(0, 10) < 4:
|
||||
loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
else:
|
||||
loc = self.multiworld.random.choice(locations_copy)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
locations_copy.remove(loc)
|
||||
if loc in overworld_locations_copy:
|
||||
overworld_locations_copy.remove(loc)
|
||||
|
||||
def place_dragons(self, rom_deltas: {int, int}):
|
||||
for i in range(3):
|
||||
table_index = static_first_dragon_index + i
|
||||
item_position_data_start = get_item_position_data_start(table_index)
|
||||
rom_deltas[item_position_data_start] = self.dragon_rooms[i]
|
||||
|
||||
def set_dragon_speeds(self, rom_deltas: {int, int}):
|
||||
rom_deltas[yorgle_speed_data_location] = self.yorgle_speed
|
||||
rom_deltas[grundle_speed_data_location] = self.grundle_speed
|
||||
rom_deltas[rhindle_speed_data_location] = self.rhindle_speed
|
||||
|
||||
def set_start_castle(self, rom_deltas):
|
||||
start_castle_value = start_castle_values[self.start_castle]
|
||||
rom_deltas[start_castle_offset] = start_castle_value
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.bin")
|
||||
foreign_item_locations: [LocationData] = []
|
||||
auto_collect_locations: [AdventureAutoCollectLocation] = []
|
||||
local_item_to_location: {int, int} = {}
|
||||
bat_no_touch_locs: [LocationData] = []
|
||||
bat_logic: int = self.multiworld.bat_logic[self.player].value
|
||||
try:
|
||||
rom_deltas: { int, int } = {}
|
||||
self.place_dragons(rom_deltas)
|
||||
self.set_dragon_speeds(rom_deltas)
|
||||
self.set_start_castle(rom_deltas)
|
||||
# start and stop indices are offsets in the ROM file, not Adventure ROM addresses (which start at f000)
|
||||
|
||||
# This places the local items (I still need to make it easy to inject the offset data)
|
||||
unplaced_local_items = dict(filter(lambda x: nothing_item_id < x[1].id <= standard_item_max,
|
||||
item_table.items()))
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
# 'nothing' items, which are autocollected when the room is entered
|
||||
if location.item.player == self.player and \
|
||||
location.item.name == "nothing":
|
||||
location_data = location_table[location.name]
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
# standard Adventure items, which are placed in the rom
|
||||
elif location.item.player == self.player and \
|
||||
location.item.name != "nothing" and \
|
||||
location.item.code is not None and \
|
||||
location.item.code <= standard_item_max:
|
||||
# I need many of the intermediate values here.
|
||||
item_table_offset = item_table[location.item.name].table_index * static_item_element_size
|
||||
item_ram_address = item_ram_addresses[item_table[location.item.name].table_index]
|
||||
item_position_data_start = item_position_table + item_ram_address - items_ram_start
|
||||
location_data = location_table[location.name]
|
||||
room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player])
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
copied_location = copy.copy(location_data)
|
||||
copied_location.local_item = item_ram_address
|
||||
bat_no_touch_locs.append(copied_location)
|
||||
del unplaced_local_items[location.item.name]
|
||||
|
||||
rom_deltas[item_position_data_start] = location_data.room_id
|
||||
rom_deltas[item_position_data_start + 1] = room_x
|
||||
rom_deltas[item_position_data_start + 2] = room_y
|
||||
local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \
|
||||
- base_location_id
|
||||
# items from other worlds, and non-standard Adventure items handled by script, like difficulty switches
|
||||
elif location.item.code is not None:
|
||||
if location.item.code != nothing_item_id:
|
||||
location_data = location_table[location.name]
|
||||
foreign_item_locations.append(location_data)
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
bat_no_touch_locs.append(location_data)
|
||||
else:
|
||||
location_data = location_table[location.name]
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
# Adventure items that are in another world get put in an invalid room until needed
|
||||
for unplaced_item_name, unplaced_item in unplaced_local_items.items():
|
||||
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
|
||||
rom_deltas[item_position_data_start] = 0xff
|
||||
|
||||
if self.multiworld.connector_multi_slot[self.player].value:
|
||||
rom_deltas[connector_port_offset] = (self.player & 0xff)
|
||||
else:
|
||||
rom_deltas[connector_port_offset] = 0
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
patch = AdventureDeltaPatch(os.path.splitext(rom_path)[0] + AdventureDeltaPatch.patch_file_ending,
|
||||
player=self.player, player_name=self.multiworld.player_name[self.player],
|
||||
locations=foreign_item_locations,
|
||||
autocollect=auto_collect_locations, local_item_locations=local_item_to_location,
|
||||
dragon_speed_reducer_info=self.dragon_speed_reducer_info,
|
||||
diff_a_mode=self.difficulty_switch_a, diff_b_mode=self.difficulty_switch_b,
|
||||
bat_logic=bat_logic, bat_no_touch_locations=bat_no_touch_locs,
|
||||
rom_deltas=rom_deltas,
|
||||
seed_name=bytes(self.multiworld.seed_name, encoding="ascii"))
|
||||
patch.write()
|
||||
finally:
|
||||
if os.path.exists(rom_path):
|
||||
os.unlink(rom_path)
|
||||
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_data: ItemData = item_table.get(name)
|
||||
return AdventureItem(name, item_data.classification, item_data.id, self.player)
|
||||
|
||||
def create_event(self, name: str, classification: ItemClassification) -> Item:
|
||||
return AdventureItem(name, classification, None, self.player)
|
||||
62
worlds/adventure/docs/en_Adventure.md
Normal file
62
worlds/adventure/docs/en_Adventure.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Adventure
|
||||
|
||||
## Where is the settings page?
|
||||
The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All
|
||||
Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized,
|
||||
slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates'
|
||||
can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist
|
||||
to reduce their speeds.
|
||||
|
||||
## What is the goal of Adventure when randomized?
|
||||
Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on
|
||||
settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
|
||||
## What is considered a location check in Adventure?
|
||||
Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item.
|
||||
A few rooms have two potential locaions. If the location contains a 'nothing' Adventure item, it will send a check when
|
||||
that is seen. If it contains an item from another Adventure or other game, it will show a rough approximation of the
|
||||
Archipelago logo that can be touched for a check. Touching a local Adventure item also 'checks' it, allowing it to be
|
||||
retrieved after a select-reset or hard reset.
|
||||
|
||||
## Why isn't my item where the spoiler says it should be?
|
||||
If something isn't where the spoiler says, most likely the bat carried it somewhere else. The bat's ability to shuffle
|
||||
items around makes it somewhat unique in Archipelago. Touching the item, wherever it is, will award the location check
|
||||
for wherever the item was originally placed.
|
||||
|
||||
## Which notable items are not randomized?
|
||||
The bat, dot, and map are not yet randomized. If the chalice is local, it is randomized, but is always in either a
|
||||
castle or the credits screen. Forcing the chalice local in the yaml is recommended.
|
||||
|
||||
## What does another world's item look like in Adventure?
|
||||
It looks vaguely like a flashing Archipelago logo.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A message is shown in the client log. While empty handed, the player can press the fire button to retrieve items in the
|
||||
order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to
|
||||
return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions.
|
||||
|
||||
## What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
## My yellow key is stuck in a wall! Am I softlocked?
|
||||
Maybe! That's all part of Adventure. If you have access to the magnet, bridge, or bat, you might be able to retrieve
|
||||
it. In general, since the bat always starts outside of castles, you should always be able to find it unless you lock
|
||||
it in a castle yourself. This mod's inventory system allows you to quickly recover all the items
|
||||
you've collected after a hard reset or select-reset (except for the dot), so usually it's not as bad as in vanilla.
|
||||
|
||||
## How do I get into the credits room? There's a item I need in there.
|
||||
Searching for 'Adventure dot map' should bring up an AtariAge map with a good walkthrough, but here's the basics.
|
||||
Bring the bridge into the black castle. Find the small room in the dungeon that cannot be reached without the bridge,
|
||||
enter it, and push yourself into the bottom right corner to pick up the dot. The dot color matches the background,
|
||||
so you won't be able to see it if it isn't in a wall, so be careful not to drop it. Bring it to the room one south and
|
||||
one east of the yellow castle and drop it there. Bring 2-3 more objects (the bat and dragons also count for this) until
|
||||
it lets you walk through the right wall.
|
||||
If the item is on the right side, you'll need the magnet to get it.
|
||||
70
worlds/adventure/docs/setup_en.md
Normal file
70
worlds/adventure/docs/setup_en.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Setup Guide for Adventure: Archipelago
|
||||
|
||||
## Important
|
||||
|
||||
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
|
||||
|
||||
## Required Software
|
||||
|
||||
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
||||
- Detailed installation instructions for Bizhawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(select `Adventure Client` during installation).
|
||||
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
|
||||
|
||||
## Configuring Bizhawk
|
||||
|
||||
Once Bizhawk has been installed, open Bizhawk and change the following settings:
|
||||
|
||||
- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
|
||||
"Lua+LuaInterface". Then restart Bizhawk. This is required for the Lua script to function correctly.
|
||||
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
|
||||
**of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
|
||||
**"NLua+KopiLua" until this step is done.**
|
||||
- Under Config > Customize, check the "Run in background" box. This will prevent disconnecting from the client while
|
||||
BizHawk is running in the background.
|
||||
|
||||
- It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Adventure patch file
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
|
||||
files. Your data file should have a `.apadvn` extension.
|
||||
|
||||
Drag your patch file to the AdventureClient.exe to start your client and start the ROM patch process. Once the process
|
||||
is finished (this can take a while), the client and the emulator will be started automatically (if you set the emulator
|
||||
path as recommended).
|
||||
|
||||
### Connect to the Multiserver
|
||||
|
||||
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
|
||||
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
|
||||
|
||||
Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`.
|
||||
|
||||
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
|
||||
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
|
||||
|
||||
Press Reset and begin playing
|
||||
Reference in New Issue
Block a user