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:
JusticePS
2023-03-22 07:25:55 -07:00
committed by GitHub
parent 206f8cf5ed
commit d48e1e447f
20 changed files with 3619 additions and 2 deletions

53
worlds/adventure/Items.py Normal file
View 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 = {
}

View 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)
}

View 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
View 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
View 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
View 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
View 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

View 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)

View 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.

View 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