Dev
This commit is contained in:
AmazingAmpharos 2018-03-19 21:52:26 -05:00 committed by GitHub
commit ae7c65aa1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 955 additions and 459 deletions

View File

@ -3,7 +3,7 @@ from enum import Enum, unique
import logging import logging
import json import json
from collections import OrderedDict from collections import OrderedDict
from Utils import int16_as_bytes
class World(object): class World(object):
@ -18,6 +18,7 @@ class World(object):
self.algorithm = algorithm self.algorithm = algorithm
self.dungeons = [] self.dungeons = []
self.regions = [] self.regions = []
self.shops = []
self.itempool = [] self.itempool = []
self.seed = None self.seed = None
self.state = CollectionState(self) self.state = CollectionState(self)
@ -379,11 +380,17 @@ class CollectionState(object):
def has_key(self, item, count=1): def has_key(self, item, count=1):
if self.world.retro: if self.world.retro:
return True #FIXME: This needs to check for shop access to a small key shop return self.can_buy_unlimited('Small Key (Universal)')
if count == 1: if count == 1:
return item in self.prog_items return item in self.prog_items
return self.item_count(item) >= count return self.item_count(item) >= count
def can_buy_unlimited(self, item):
for shop in self.world.shops:
if shop.has_unlimited(item) and shop.region.can_reach(self):
return True
return False
def item_count(self, item): def item_count(self, item):
return len([pritem for pritem in self.prog_items if pritem == item]) return len([pritem for pritem in self.prog_items if pritem == item])
@ -424,18 +431,25 @@ class CollectionState(object):
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count()) basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count())
elif self.world.difficulty == 'insane' and not fullrefill: elif self.world.difficulty == 'insane' and not fullrefill:
basemagic = basemagic basemagic = basemagic
else: elif self.can_buy_unlimited('Green Potion') or self.can_buy_unlimited('Red Potion'):
basemagic = basemagic + basemagic * self.bottle_count() basemagic = basemagic + basemagic * self.bottle_count()
return basemagic >= smallmagic # FIXME bottle should really also have a requirement that we can reach some shop that sells green or blue potions return basemagic >= smallmagic
def can_kill_most_things(self, enemies=5): def can_kill_most_things(self, enemies=5):
return (self.has_blunt_weapon() return (self.has_blunt_weapon()
or self.has('Cane of Somaria') or self.has('Cane of Somaria')
or (self.has('Cane of Byrna') and (enemies < 6 or self.can_extend_Magic())) or (self.has('Cane of Byrna') and (enemies < 6 or self.can_extend_Magic()))
or self.has('Bow') or self.can_shoot_arrows()
or self.has('Fire Rod') or self.has('Fire Rod')
) )
def can_shoot_arrows(self):
if self.world.retro:
#TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location)
#FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe
return self.has('Bow') and (self.has('Silver Arrows') or self.can_buy_unlimited('Single Arrow'))
return self.has('Bow')
def has_sword(self): def has_sword(self):
return self.has('Fighter Sword') or self.has('Master Sword') or self.has('Tempered Sword') or self.has('Golden Sword') return self.has('Fighter Sword') or self.has('Master Sword') or self.has('Tempered Sword') or self.has('Golden Sword')
@ -574,7 +588,6 @@ class RegionType(Enum):
return self in (RegionType.Cave, RegionType.Dungeon) return self in (RegionType.Cave, RegionType.Dungeon)
class Region(object): class Region(object):
def __init__(self, name, type): def __init__(self, name, type):
@ -584,6 +597,7 @@ class Region(object):
self.exits = [] self.exits = []
self.locations = [] self.locations = []
self.dungeon = None self.dungeon = None
self.shop = None
self.world = None self.world = None
self.is_light_world = False # will be set aftermaking connections. self.is_light_world = False # will be set aftermaking connections.
self.is_dark_world = False self.is_dark_world = False
@ -751,6 +765,64 @@ class Item(object):
class Crystal(Item): class Crystal(Item):
pass pass
@unique
class ShopType(Enum):
Shop = 0
TakeAny = 1
class Shop(object):
def __init__(self, region, room_id, type, shopkeeper_config, replaceable):
self.region = region
self.room_id = room_id
self.type = type
self.inventory = [None, None, None]
self.shopkeeper_config = shopkeeper_config
self.replaceable = replaceable
self.active = False
@property
def item_count(self):
return (3 if self.inventory[2] else
2 if self.inventory[1] else
1 if self.inventory[0] else
0)
def get_bytes(self):
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
entrances = self.region.entrances
config = self.item_count
if len(entrances) == 1 and entrances[0].addresses:
door_id = entrances[0].addresses+1
else:
door_id = 0
config |= 0x40 # ignore door id
if self.type == ShopType.TakeAny:
config |= 0x80
return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00]
def has_unlimited(self, item):
for inv in self.inventory:
if inv is None:
continue
if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item:
return True
elif inv['item'] is not None and inv['item'] == item:
return True
return False
def clear_inventory(self):
self.inventory = [None, None, None]
def add_inventory(self, slot, item, price, max=0, replacement=None, replacement_price=0, create_location=False):
self.inventory[slot] = {
'item': item,
'price': price,
'max': max,
'replacement': replacement,
'replacement_price': replacement_price,
'create_location': create_location
}
class Spoiler(object): class Spoiler(object):

View File

@ -1026,7 +1026,7 @@ def link_entrances(world):
if world.get_entrance('Dam').connected_region.name != 'Dam' or world.get_entrance('Swamp Palace').connected_region.name != 'Swamp Palace (Entrance)': if world.get_entrance('Dam').connected_region.name != 'Dam' or world.get_entrance('Swamp Palace').connected_region.name != 'Swamp Palace (Entrance)':
world.swamp_patch_required = True world.swamp_patch_required = True
# check for # check for potion shop location
if world.get_entrance('Potion Shop').connected_region.name != 'Potion Shop': if world.get_entrance('Potion Shop').connected_region.name != 'Potion Shop':
world.powder_patch_required = True world.powder_patch_required = True
@ -1744,6 +1744,7 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central
('Paradox Cave Push Block', 'Paradox Cave Front'), ('Paradox Cave Push Block', 'Paradox Cave Front'),
('Paradox Cave Bomb Jump', 'Paradox Cave'), ('Paradox Cave Bomb Jump', 'Paradox Cave'),
('Paradox Cave Drop', 'Paradox Cave Chest Area'), ('Paradox Cave Drop', 'Paradox Cave Chest Area'),
('Light World Death Mountain Shop', 'Light World Death Mountain Shop'),
('Fairy Ascension Rocks', 'Fairy Ascension Plateau'), ('Fairy Ascension Rocks', 'Fairy Ascension Plateau'),
('Fairy Ascension Mirror Spot', 'Fairy Ascension Plateau'), ('Fairy Ascension Mirror Spot', 'Fairy Ascension Plateau'),
('Fairy Ascension Drop', 'East Death Mountain (Bottom)'), ('Fairy Ascension Drop', 'East Death Mountain (Bottom)'),

View File

@ -217,6 +217,14 @@ def generate_itempool(world):
world.get_location('Agahnim 1').event = True world.get_location('Agahnim 1').event = True
world.push_item('Agahnim 2', ItemFactory('Beat Agahnim 2'), False) world.push_item('Agahnim 2', ItemFactory('Beat Agahnim 2'), False)
world.get_location('Agahnim 2').event = True world.get_location('Agahnim 2').event = True
world.push_item('Dark Blacksmith Ruins', ItemFactory('Pick Up Purple Chest'), False)
world.get_location('Dark Blacksmith Ruins').event = True
world.push_item('Frog', ItemFactory('Get Frog'), False)
world.get_location('Frog').event = True
world.push_item('Missing Smith', ItemFactory('Return Smith'), False)
world.get_location('Missing Smith').event = True
world.push_item('Floodgate', ItemFactory('Open Floodgate'), False)
world.get_location('Floodgate').event = True
# set up item pool # set up item pool
if world.custom: if world.custom:
@ -254,6 +262,8 @@ def generate_itempool(world):
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
world.required_medallions = (mm_medallion, tr_medallion) world.required_medallions = (mm_medallion, tr_medallion)
set_up_shops(world)
# distribute crystals # distribute crystals
fill_prizes(world) fill_prizes(world)
@ -284,7 +294,23 @@ def fill_prizes(world, attempts=15):
raise FillError('Unable to place dungeon prizes') raise FillError('Unable to place dungeon prizes')
def set_up_shops(world):
# Changes to basic Shops
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
if world.retro:
rss = world.get_region('Red Shield Shop').shop
rss.active = True
rss.add_inventory(2, 'Single Arrow', 80)
# Randomized changes to Shops
if world.retro:
for shop in random.sample([s for s in world.shops if s.replaceable], 5):
shop.active = True
shop.add_inventory(0, 'Single Arrow', 80)
shop.add_inventory(1, 'Small Key (Universal)', 100)
#special shop types
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro): def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro):
pool = [] pool = []
@ -558,6 +584,7 @@ def test():
for mode in ['open', 'standard', 'swordless']: for mode in ['open', 'standard', 'swordless']:
for progressive in ['on', 'off']: for progressive in ['on', 'off']:
for shuffle in ['full', 'insane']: for shuffle in ['full', 'insane']:
for retro in [True, False]:
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro) out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro)
count = len(out[0]) + len(out[1]) count = len(out[0]) + len(out[1])
@ -568,7 +595,7 @@ def test():
if retro: if retro:
correct_count += 28 correct_count += 28
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode)) assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, retro))
if __name__ == '__main__': if __name__ == '__main__':
test() test()

View File

@ -160,5 +160,15 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'), 'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'), 'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again'), 'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again'),
'Red Potion': (False, False, None, 0x2E, None, None, None, None, None, None),
'Green Potion': (False, False, None, 0x2F, None, None, None, None, None, None),
'Blue Potion': (False, False, None, 0x30, None, None, None, None, None, None),
'Bee': (False, False, None, 0x0E, None, None, None, None, None, None),
'Small Heart': (False, False, None, 0x42, None, None, None, None, None, None),
'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None), 'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None),
'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None)} 'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None),
'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None),
'Return Smith': (True, False, 'Event', None, None, None, None, None, None, None),
'Pick Up Purple Chest': (True, False, 'Event', None, None, None, None, None, None, None),
'Open Floodgate': (True, False, 'Event', None, None, None, None, None, None, None),
}

12
Main.py
View File

@ -1,4 +1,5 @@
from collections import OrderedDict from collections import OrderedDict
import copy
from itertools import zip_longest from itertools import zip_longest
import json import json
import logging import logging
@ -157,6 +158,15 @@ def copy_world(world):
create_regions(ret) create_regions(ret)
create_dungeons(ret) create_dungeons(ret)
#TODO: copy_dynamic_regions_and_locations() # also adds some new shops
for shop in world.shops:
copied_shop = ret.get_region(shop.region.name).shop
copied_shop.active = shop.active
copied_shop.inventory = copy.copy(shop.inventory)
# connect copied world # connect copied world
for region in world.regions: for region in world.regions:
copied_region = ret.get_region(region.name) copied_region = ret.get_region(region.name)
@ -294,8 +304,6 @@ def create_playthrough(world):
old_world.spoiler.paths = {location.name : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere} old_world.spoiler.paths = {location.name : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere}
if any(exit == 'Pyramid Fairy' for path in old_world.spoiler.paths.values() for (_, exit) in path): if any(exit == 'Pyramid Fairy' for path in old_world.spoiler.paths.values() for (_, exit) in path):
old_world.spoiler.paths['Big Bomb Shop'] = get_path(state, world.get_region('Big Bomb Shop')) old_world.spoiler.paths['Big Bomb Shop'] = get_path(state, world.get_region('Big Bomb Shop'))
if any(exit == 'Swamp Palace Moat' for path in old_world.spoiler.paths.values() for (_, exit) in path) or 'Sunken Treasure' in old_world.required_locations:
old_world.spoiler.paths['Dam'] = get_path(state, world.get_region('Dam'))
# we can finally output our playthrough # we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)]) old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])

View File

@ -1,5 +1,5 @@
import collections import collections
from BaseClasses import Region, Location, Entrance, RegionType from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
def create_regions(world): def create_regions(world):
@ -28,7 +28,7 @@ def create_regions(world):
create_lw_region('Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), create_lw_region('Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region('Kings Grave', ['King\'s Tomb']), create_cave_region('Kings Grave', ['King\'s Tomb']),
create_cave_region('North Fairy Cave', None, ['North Fairy Cave Exit']), create_cave_region('North Fairy Cave', None, ['North Fairy Cave Exit']),
create_cave_region('Dam', ['Floodgate Chest']), create_cave_region('Dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region('Links House', ['Link\'s House'], ['Links House Exit']), create_cave_region('Links House', ['Link\'s House'], ['Links House Exit']),
create_cave_region('Chris Houlihan Room', None, ['Chris Houlihan Room Exit']), create_cave_region('Chris Houlihan Room', None, ['Chris Houlihan Room Exit']),
create_cave_region('Tavern', ['Kakariko Tavern']), create_cave_region('Tavern', ['Kakariko Tavern']),
@ -57,7 +57,7 @@ def create_regions(world):
create_cave_region('Kakariko Well (top)', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', create_cave_region('Kakariko Well (top)', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region('Kakariko Well (bottom)', None, ['Kakariko Well Exit']), create_cave_region('Kakariko Well (bottom)', None, ['Kakariko Well Exit']),
create_cave_region('Blacksmiths Hut', ['Blacksmith']), create_cave_region('Blacksmiths Hut', ['Blacksmith', 'Missing Smith']),
create_lw_region('Bat Cave Drop Ledge', None, ['Bat Cave Drop']), create_lw_region('Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region('Bat Cave (right)', ['Magic Bat'], ['Bat Cave Door']), create_cave_region('Bat Cave (right)', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region('Bat Cave (left)', None, ['Bat Cave Exit']), create_cave_region('Bat Cave (left)', None, ['Bat Cave Exit']),
@ -125,7 +125,7 @@ def create_regions(world):
create_cave_region('Spectacle Rock Cave (Peak)', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), create_cave_region('Spectacle Rock Cave (Peak)', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_lw_region('East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), create_lw_region('East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region('Hookshot Fairy'), create_cave_region('Hookshot Fairy'),
create_cave_region('Paradox Cave Front', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)']), create_cave_region('Paradox Cave Front', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
create_cave_region('Paradox Cave Chest Area', ['Paradox Cave Lower - Far Left', create_cave_region('Paradox Cave Chest Area', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left', 'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right', 'Paradox Cave Lower - Right',
@ -135,6 +135,7 @@ def create_regions(world):
'Paradox Cave Upper - Right'], 'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region('Paradox Cave', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), create_cave_region('Paradox Cave', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
create_cave_region('Light World Death Mountain Shop'),
create_lw_region('East Death Mountain (Top)', None, ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']), create_lw_region('East Death Mountain (Top)', None, ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
create_lw_region('Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']), create_lw_region('Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
create_cave_region('Spiral Cave (Top)', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), create_cave_region('Spiral Cave (Top)', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
@ -164,10 +165,10 @@ def create_regions(world):
create_cave_region('Dark Lake Hylia Ledge Spike Cave'), create_cave_region('Dark Lake Hylia Ledge Spike Cave'),
create_cave_region('Hype Cave', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', create_cave_region('Hype Cave', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
create_dw_region('West Dark World', None, ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Bumper Cave Entrance Rock', create_dw_region('West Dark World', ['Frog'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Bumper Cave Entrance Rock',
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop']), 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop']),
create_dw_region('Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop']), create_dw_region('Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop']),
create_dw_region('Hammer Peg Area', None, ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']), create_dw_region('Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
create_dw_region('Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']), create_dw_region('Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
create_cave_region('Fortune Teller (Dark)'), create_cave_region('Fortune Teller (Dark)'),
create_cave_region('Village of Outcasts Shop'), create_cave_region('Village of Outcasts Shop'),
@ -290,6 +291,14 @@ def create_regions(world):
create_dw_region('Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']) create_dw_region('Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop'])
] ]
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
region = world.get_region(region_name)
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
region.shop = shop
world.shops.append(shop)
for index, (item, price) in enumerate(default_shop_contents[region_name]):
shop.add_inventory(index, item, price)
world.intialize_regions() world.intialize_regions()
def create_lw_region(name, locations=None, exits=None): def create_lw_region(name, locations=None, exits=None):
@ -347,6 +356,38 @@ def mark_light_world_regions(world):
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
# (room_id, shopkeeper, replaceable)
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, 0x51, True),
'Red Shield Shop': (0x0110, 0x51, True),
'Dark Lake Hylia Shop': (0x010F, 0x51, True),
'Dark World Lumberjack Shop': (0x010F, 0x51, True),
'Village of Outcasts Shop': (0x010F, 0x51, True),
'Dark World Potion Shop': (0x010F, 0x51, True),
'Light World Death Mountain Shop': (0x00FF, 0x51, True),
'Kakariko Shop': (0x011F, 0x51, True),
'Cave Shop (Lake Hylia)': (0x0112, 0x51, True),
'Potion Shop': (0x0109, 0xFF, False),
# Bomb Shop not currently modeled as a shop, due to special nature of items
}
# region, [item]
# slot, item, price, max=0, replacement=None, replacement_price=0
# item = (item, price)
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
default_shop_contents = {
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
'Village of Outcasts Shop': _dark_world_shop_defaults,
'Dark World Potion Shop': _dark_world_shop_defaults,
'Light World Death Mountain Shop': _basic_shop_defaults,
'Kakariko Shop': _basic_shop_defaults,
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
}
location_table = {'Mushroom': (0x180013, False, 'in the woods'), location_table = {'Mushroom': (0x180013, False, 'in the woods'),
'Bottle Merchant': (0x2EB18, False, 'with a merchant'), 'Bottle Merchant': (0x2EB18, False, 'with a merchant'),
@ -567,6 +608,10 @@ location_table = {'Mushroom': (0x180013, False, 'in the woods'),
'Ganon': (None, False, 'from me'), 'Ganon': (None, False, 'from me'),
'Agahnim 1': (None, False, 'from my wizardry form'), 'Agahnim 1': (None, False, 'from my wizardry form'),
'Agahnim 2': (None, False, 'from my wizardry form'), 'Agahnim 2': (None, False, 'from my wizardry form'),
'Floodgate': (None, False, None),
'Frog': (None, False, None),
'Missing Smith': (None, False, None),
'Dark Blacksmith Ruins': (None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'), 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'), 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'), 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'),

67
Rom.py
View File

@ -6,16 +6,17 @@ import os
import struct import struct
import random import random
from BaseClasses import ShopType
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Text import string_to_alttp_text, text_addresses, Credits from Text import MultiByteTextMapper, text_addresses, Credits
from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import local_path from Utils import local_path, int16_as_bytes, int32_as_bytes
from Items import ItemFactory from Items import ItemFactory
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'dc5840f0d1ef7b51009c5625a054b3dd' RANDOMIZERBASEHASH = '354bad0d01b6284c89f05c9b414d48c1'
class JsonRom(object): class JsonRom(object):
@ -259,15 +260,6 @@ class Sprite(object):
# split into palettes of 15 colors # split into palettes of 15 colors
return array_chunk(palette_as_colors, 15) return array_chunk(palette_as_colors, 15)
def int16_as_bytes(value):
value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF]
def int32_as_bytes(value):
value = value & 0xFFFFFFFF
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None): def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
# patch items # patch items
for location in world.get_locations(): for location in world.get_locations():
@ -355,6 +347,7 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
# patch door table # patch door table
rom.write_byte(0xDBB73 + exit.addresses, exit.target) rom.write_byte(0xDBB73 + exit.addresses, exit.target)
write_custom_shops(rom, world)
# patch medallion requirements # patch medallion requirements
if world.required_medallions[0] == 'Bombos': if world.required_medallions[0] == 'Bombos':
@ -567,6 +560,19 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8, 0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB] 0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8,
0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA,
0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC,
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
if world.retro:
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
0xE2: 0xDB} #10 Arrows -> Red Rupee
prizes = [prize_replacements.get(prize,prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize,prize) for prize in dig_prizes]
rom.write_bytes(0x180100, dig_prizes)
random.shuffle(prizes) random.shuffle(prizes)
# write tree pull prizes # write tree pull prizes
@ -808,9 +814,6 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
# Shop table
rom.write_bytes(0x184800, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved # patch swamp: Need to enable permanent drain of water as dam or swamp were moved
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required else 0x00) rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required else 0x00)
@ -851,6 +854,38 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
return rom return rom
def write_custom_shops(rom, world):
shops = [shop for shop in world.shops if shop.replaceable and shop.active]
shop_data = bytearray()
items_data = bytearray()
sram_offset = 0
for shop_id, shop in enumerate(shops):
if shop_id == len(shops) - 1:
shop_id = 0xFF
bytes = shop.get_bytes()
bytes[0] = shop_id
bytes[-1] = sram_offset
if shop.type == ShopType.TakeAny:
sram_offset += 1
else:
sram_offset += shop.item_count
shop_data.extend(bytes)
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high]
for item in shop.inventory:
if item is None:
break
item_data = [shop_id, ItemFactory(item['item']).code] + int16_as_bytes(item['price']) + [item['max'], ItemFactory(item['replacement']).code if item['replacement'] else 0xFF] + int16_as_bytes(item['replacement_price'])
items_data.extend(item_data)
rom.write_bytes(0x184800, shop_data)
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
rom.write_bytes(0x184900, items_data)
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite):
# enable instant item menu # enable instant item menu
@ -979,7 +1014,7 @@ def write_sprite(rom, sprite):
def write_string_to_rom(rom, target, string): def write_string_to_rom(rom, target, string):
address, maxbytes = text_addresses[target] address, maxbytes = text_addresses[target]
rom.write_bytes(address, string_to_alttp_text(string, maxbytes)) rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom, world): def write_strings(rom, world):

View File

@ -110,11 +110,14 @@ def global_rules(world):
set_rule(world.get_entrance('South Hyrule Teleporter'), lambda state: state.has('Hammer') and state.can_lift_rocks() and state.has_Pearl()) # bunny cannot use hammer set_rule(world.get_entrance('South Hyrule Teleporter'), lambda state: state.has('Hammer') and state.can_lift_rocks() and state.has_Pearl()) # bunny cannot use hammer
set_rule(world.get_entrance('Kakariko Teleporter'), lambda state: ((state.has('Hammer') and state.can_lift_rocks()) or state.can_lift_heavy_rocks()) and state.has_Pearl()) # bunny cannot lift bushes set_rule(world.get_entrance('Kakariko Teleporter'), lambda state: ((state.has('Hammer') and state.can_lift_rocks()) or state.can_lift_heavy_rocks()) and state.has_Pearl()) # bunny cannot lift bushes
set_rule(world.get_location('Flute Spot'), lambda state: state.has('Shovel')) set_rule(world.get_location('Flute Spot'), lambda state: state.has('Shovel'))
set_rule(world.get_location('Purple Chest'), lambda state: state.can_reach('Blacksmith', 'Location')) # Can S&Q with chest set_rule(world.get_location('Dark Blacksmith Ruins'), lambda state: state.has('Return Smith'))
set_rule(world.get_location('Purple Chest'), lambda state: state.has('Pick Up Purple Chest')) # Can S&Q with chest
set_rule(world.get_location('Zora\'s Ledge'), lambda state: state.has('Flippers')) set_rule(world.get_location('Zora\'s Ledge'), lambda state: state.has('Flippers'))
set_rule(world.get_entrance('Waterfall of Wishing'), lambda state: state.has('Flippers')) # can be fake flippered into, but is in weird state inside that might prevent you from doing things. Can be improved in future Todo set_rule(world.get_entrance('Waterfall of Wishing'), lambda state: state.has('Flippers')) # can be fake flippered into, but is in weird state inside that might prevent you from doing things. Can be improved in future Todo
set_rule(world.get_location('Blacksmith'), lambda state: state.can_lift_heavy_rocks() and state.can_reach('West Dark World') and state.has_Pearl()) # Can S&Q with smith set_rule(world.get_location('Frog'), lambda state: state.can_lift_heavy_rocks()) # will get automatic moon pearl requirement
set_rule(world.get_location('Missing Smith'), lambda state: state.has('Get Frog')) # Can S&Q with smith
set_rule(world.get_location('Blacksmith'), lambda state: state.has('Return Smith'))
set_rule(world.get_location('Magic Bat'), lambda state: state.has('Magic Powder')) set_rule(world.get_location('Magic Bat'), lambda state: state.has('Magic Powder'))
set_rule(world.get_location('Sick Kid'), lambda state: state.has_bottle()) set_rule(world.get_location('Sick Kid'), lambda state: state.has_bottle())
set_rule(world.get_location('Library'), lambda state: state.has_Boots()) set_rule(world.get_location('Library'), lambda state: state.has_Boots())
@ -216,8 +219,8 @@ def global_rules(world):
set_rule(world.get_entrance('Sewers Back Door'), lambda state: state.has_key('Small Key (Escape)')) set_rule(world.get_entrance('Sewers Back Door'), lambda state: state.has_key('Small Key (Escape)'))
set_rule(world.get_location('Eastern Palace - Big Chest'), lambda state: state.has('Big Key (Eastern Palace)')) set_rule(world.get_location('Eastern Palace - Big Chest'), lambda state: state.has('Big Key (Eastern Palace)'))
set_rule(world.get_location('Eastern Palace - Armos Knights'), lambda state: state.has('Bow') and state.has('Big Key (Eastern Palace)')) set_rule(world.get_location('Eastern Palace - Armos Knights'), lambda state: state.can_shoot_arrows() and state.has('Big Key (Eastern Palace)'))
set_rule(world.get_location('Eastern Palace - Prize'), lambda state: state.has('Bow') and state.has('Big Key (Eastern Palace)')) set_rule(world.get_location('Eastern Palace - Prize'), lambda state: state.can_shoot_arrows() and state.has('Big Key (Eastern Palace)'))
for location in ['Eastern Palace - Armos Knights', 'Eastern Palace - Big Chest']: for location in ['Eastern Palace - Armos Knights', 'Eastern Palace - Big Chest']:
forbid_item(world.get_location(location), 'Big Key (Eastern Palace)') forbid_item(world.get_location(location), 'Big Key (Eastern Palace)')
@ -225,9 +228,9 @@ def global_rules(world):
set_rule(world.get_location('Desert Palace - Torch'), lambda state: state.has_Boots()) set_rule(world.get_location('Desert Palace - Torch'), lambda state: state.has_Boots())
set_rule(world.get_entrance('Desert Palace East Wing'), lambda state: state.has_key('Small Key (Desert Palace)')) set_rule(world.get_entrance('Desert Palace East Wing'), lambda state: state.has_key('Small Key (Desert Palace)'))
set_rule(world.get_location('Desert Palace - Prize'), lambda state: state.has_key('Small Key (Desert Palace)') and state.has('Big Key (Desert Palace)') and state.has_fire_source() and set_rule(world.get_location('Desert Palace - Prize'), lambda state: state.has_key('Small Key (Desert Palace)') and state.has('Big Key (Desert Palace)') and state.has_fire_source() and
(state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Bow'))) (state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.can_shoot_arrows()))
set_rule(world.get_location('Desert Palace - Lanmolas'), lambda state: state.has_key('Small Key (Desert Palace)') and state.has('Big Key (Desert Palace)') and state.has_fire_source() and set_rule(world.get_location('Desert Palace - Lanmolas'), lambda state: state.has_key('Small Key (Desert Palace)') and state.has('Big Key (Desert Palace)') and state.has_fire_source() and
(state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Bow'))) (state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.can_shoot_arrows()))
for location in ['Desert Palace - Lanmolas', 'Desert Palace - Big Chest']: for location in ['Desert Palace - Lanmolas', 'Desert Palace - Big Chest']:
forbid_item(world.get_location(location), 'Big Key (Desert Palace)') forbid_item(world.get_location(location), 'Big Key (Desert Palace)')
@ -246,7 +249,9 @@ def global_rules(world):
# for location in ['Tower of Hera - Big Key Chest']: # for location in ['Tower of Hera - Big Key Chest']:
# forbid_item(world.get_location(location), 'Small Key (Tower of Hera)') # forbid_item(world.get_location(location), 'Small Key (Tower of Hera)')
set_rule(world.get_entrance('Swamp Palace Moat'), lambda state: state.has('Flippers') and state.can_reach('Dam')) set_rule(world.get_entrance('Swamp Palace Moat'), lambda state: state.has('Flippers') and state.has('Open Floodgate'))
add_rule(world.get_location('Sunken Treasure'), lambda state: state.has('Open Floodgate'))
set_rule(world.get_entrance('Swamp Palace Small Key Door'), lambda state: state.has_key('Small Key (Swamp Palace)')) set_rule(world.get_entrance('Swamp Palace Small Key Door'), lambda state: state.has_key('Small Key (Swamp Palace)'))
set_rule(world.get_entrance('Swamp Palace (Center)'), lambda state: state.has('Hammer')) set_rule(world.get_entrance('Swamp Palace (Center)'), lambda state: state.has('Hammer'))
set_rule(world.get_location('Swamp Palace - Big Chest'), lambda state: state.has('Big Key (Swamp Palace)') or item_name(state, 'Swamp Palace - Big Chest') == 'Big Key (Swamp Palace)') set_rule(world.get_location('Swamp Palace - Big Chest'), lambda state: state.has('Big Key (Swamp Palace)') or item_name(state, 'Swamp Palace - Big Chest') == 'Big Key (Swamp Palace)')
@ -285,7 +290,7 @@ def global_rules(world):
for location in ['Ice Palace - Big Chest', 'Ice Palace - Kholdstare']: for location in ['Ice Palace - Big Chest', 'Ice Palace - Kholdstare']:
forbid_item(world.get_location(location), 'Big Key (Ice Palace)') forbid_item(world.get_location(location), 'Big Key (Ice Palace)')
set_rule(world.get_entrance('Misery Mire Entrance Gap'), lambda state: (state.has_Boots() or state.has('Hookshot')) and (state.has_sword() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Hammer') or state.has('Cane of Somaria') or state.has('Bow'))) # need to defeat wizzrobes, bombs don't work ... set_rule(world.get_entrance('Misery Mire Entrance Gap'), lambda state: (state.has_Boots() or state.has('Hookshot')) and (state.has_sword() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Hammer') or state.has('Cane of Somaria') or state.can_shoot_arrows())) # need to defeat wizzrobes, bombs don't work ...
set_rule(world.get_location('Misery Mire - Big Chest'), lambda state: state.has('Big Key (Misery Mire)')) set_rule(world.get_location('Misery Mire - Big Chest'), lambda state: state.has('Big Key (Misery Mire)'))
set_rule(world.get_location('Misery Mire - Spike Chest'), lambda state: (state.world.can_take_damage and state.has_hearts(4)) or state.has('Cane of Byrna') or state.has('Cape')) set_rule(world.get_location('Misery Mire - Spike Chest'), lambda state: (state.world.can_take_damage and state.has_hearts(4)) or state.has('Cane of Byrna') or state.has('Cape'))
set_rule(world.get_entrance('Misery Mire Big Key Door'), lambda state: state.has('Big Key (Misery Mire)')) set_rule(world.get_entrance('Misery Mire Big Key Door'), lambda state: state.has('Big Key (Misery Mire)'))
@ -299,7 +304,7 @@ def global_rules(world):
(item_name(state, 'Misery Mire - Big Key Chest') in ['Big Key (Misery Mire)'])) else state.has_key('Small Key (Misery Mire)', 3)) (item_name(state, 'Misery Mire - Big Key Chest') in ['Big Key (Misery Mire)'])) else state.has_key('Small Key (Misery Mire)', 3))
set_rule(world.get_location('Misery Mire - Compass Chest'), lambda state: state.has_fire_source()) set_rule(world.get_location('Misery Mire - Compass Chest'), lambda state: state.has_fire_source())
set_rule(world.get_location('Misery Mire - Big Key Chest'), lambda state: state.has_fire_source()) set_rule(world.get_location('Misery Mire - Big Key Chest'), lambda state: state.has_fire_source())
set_rule(world.get_entrance('Misery Mire (Vitreous)'), lambda state: state.has('Cane of Somaria') and (state.has('Bow') or state.has_blunt_weapon())) set_rule(world.get_entrance('Misery Mire (Vitreous)'), lambda state: state.has('Cane of Somaria') and (state.can_shoot_arrows() or state.has_blunt_weapon()))
for location in ['Misery Mire - Big Chest', 'Misery Mire - Vitreous']: for location in ['Misery Mire - Big Chest', 'Misery Mire - Vitreous']:
forbid_item(world.get_location(location), 'Big Key (Misery Mire)') forbid_item(world.get_location(location), 'Big Key (Misery Mire)')
@ -323,10 +328,10 @@ def global_rules(world):
set_trock_key_rules(world) set_trock_key_rules(world)
set_rule(world.get_entrance('Palace of Darkness Bonk Wall'), lambda state: state.has('Bow')) set_rule(world.get_entrance('Palace of Darkness Bonk Wall'), lambda state: state.can_shoot_arrows())
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop'), lambda state: state.has('Hammer')) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop'), lambda state: state.has('Hammer'))
set_rule(world.get_entrance('Palace of Darkness Bridge Room'), lambda state: state.has_key('Small Key (Palace of Darkness)', 1)) # If we can reach any other small key door, we already have back door access to this area set_rule(world.get_entrance('Palace of Darkness Bridge Room'), lambda state: state.has_key('Small Key (Palace of Darkness)', 1)) # If we can reach any other small key door, we already have back door access to this area
set_rule(world.get_entrance('Palace of Darkness Big Key Door'), lambda state: state.has_key('Small Key (Palace of Darkness)', 6) and state.has('Big Key (Palace of Darkness)') and state.has('Bow') and state.has('Hammer')) set_rule(world.get_entrance('Palace of Darkness Big Key Door'), lambda state: state.has_key('Small Key (Palace of Darkness)', 6) and state.has('Big Key (Palace of Darkness)') and state.can_shoot_arrows() and state.has('Hammer'))
set_rule(world.get_entrance('Palace of Darkness (North)'), lambda state: state.has_key('Small Key (Palace of Darkness)', 4)) set_rule(world.get_entrance('Palace of Darkness (North)'), lambda state: state.has_key('Small Key (Palace of Darkness)', 4))
set_rule(world.get_location('Palace of Darkness - Big Chest'), lambda state: state.has('Big Key (Palace of Darkness)')) set_rule(world.get_location('Palace of Darkness - Big Chest'), lambda state: state.has('Big Key (Palace of Darkness)'))
@ -366,10 +371,10 @@ def global_rules(world):
set_rule(world.get_location(location), lambda state: state.has('Fire Rod') and (state.has_key('Small Key (Ganons Tower)', 4) or (item_in_locations(state, 'Big Key (Ganons Tower)', compass_room_chests) and state.has_key('Small Key (Ganons Tower)', 3)))) set_rule(world.get_location(location), lambda state: state.has('Fire Rod') and (state.has_key('Small Key (Ganons Tower)', 4) or (item_in_locations(state, 'Big Key (Ganons Tower)', compass_room_chests) and state.has_key('Small Key (Ganons Tower)', 3))))
set_rule(world.get_location('Ganons Tower - Big Chest'), lambda state: state.has('Big Key (Ganons Tower)')) set_rule(world.get_location('Ganons Tower - Big Chest'), lambda state: state.has('Big Key (Ganons Tower)'))
set_rule(world.get_location('Ganons Tower - Big Key Room - Left'), lambda state: state.has('Bow') or state.has_blunt_weapon()) set_rule(world.get_location('Ganons Tower - Big Key Room - Left'), lambda state: state.can_shoot_arrows() or state.has_blunt_weapon())
set_rule(world.get_location('Ganons Tower - Big Key Chest'), lambda state: state.has('Bow') or state.has_blunt_weapon()) set_rule(world.get_location('Ganons Tower - Big Key Chest'), lambda state: state.can_shoot_arrows() or state.has_blunt_weapon())
set_rule(world.get_location('Ganons Tower - Big Key Room - Right'), lambda state: state.has('Bow') or state.has_blunt_weapon()) set_rule(world.get_location('Ganons Tower - Big Key Room - Right'), lambda state: state.can_shoot_arrows() or state.has_blunt_weapon())
set_rule(world.get_entrance('Ganons Tower Big Key Door'), lambda state: state.has('Big Key (Ganons Tower)') and state.has('Bow')) set_rule(world.get_entrance('Ganons Tower Big Key Door'), lambda state: state.has('Big Key (Ganons Tower)') and state.can_shoot_arrows())
set_rule(world.get_entrance('Ganons Tower Torch Rooms'), lambda state: state.has_fire_source()) set_rule(world.get_entrance('Ganons Tower Torch Rooms'), lambda state: state.has_fire_source())
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest'), lambda state: state.has_key('Small Key (Ganons Tower)', 3)) set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest'), lambda state: state.has_key('Small Key (Ganons Tower)', 3))
set_rule(world.get_entrance('Ganons Tower Moldorm Door'), lambda state: state.has_key('Small Key (Ganons Tower)', 4)) set_rule(world.get_entrance('Ganons Tower Moldorm Door'), lambda state: state.has_key('Small Key (Ganons Tower)', 4))
@ -382,7 +387,7 @@ def global_rules(world):
set_rule(world.get_location('Ganon'), lambda state: state.has_beam_sword() and state.has_fire_source() and state.has('Crystal 1') and state.has('Crystal 2') set_rule(world.get_location('Ganon'), lambda state: state.has_beam_sword() and state.has_fire_source() and state.has('Crystal 1') and state.has('Crystal 2')
and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7') and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7')
and (state.has('Tempered Sword') or state.has('Golden Sword') or (state.has('Silver Arrows') and state.has('Bow')) or state.has('Lamp') or state.can_extend_magic(12))) # need to light torch a sufficient amount of times and (state.has('Tempered Sword') or state.has('Golden Sword') or (state.has('Silver Arrows') and state.can_shoot_arrows()) or state.has('Lamp') or state.can_extend_magic(12))) # need to light torch a sufficient amount of times
set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has_beam_sword()) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has_beam_sword()) # need to damage ganon to get tiles to drop
@ -459,7 +464,7 @@ def swordless_rules(world):
open_rules(world) open_rules(world)
set_rule(world.get_entrance('Agahnims Tower'), lambda state: state.has('Cape') or state.has('Hammer') or state.has('Beat Agahnim 1')) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Agahnims Tower'), lambda state: state.has('Cape') or state.has('Hammer') or state.has('Beat Agahnim 1')) # barrier gets removed after killing agahnim, relevant for entrance shuffle
set_rule(world.get_entrance('Agahnim 1'), lambda state: (state.has('Hammer') or (state.has('Bug Catching Net') and (state.has('Fire Rod') or state.has('Bow') or state.has('Cane of Somaria')))) and state.has_key('Small Key (Agahnims Tower)', 2)) set_rule(world.get_entrance('Agahnim 1'), lambda state: (state.has('Hammer') or (state.has('Bug Catching Net') and (state.has('Fire Rod') or state.can_shoot_arrows() or state.has('Cane of Somaria')))) and state.has_key('Small Key (Agahnims Tower)', 2))
set_rule(world.get_location('Ether Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer')) set_rule(world.get_location('Ether Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer'))
set_rule(world.get_location('Bombos Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer') and state.has_Mirror()) set_rule(world.get_location('Bombos Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer') and state.has_Mirror())
set_rule(world.get_entrance('Misery Mire'), lambda state: state.has_Pearl() and state.has_misery_mire_medallion()) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Misery Mire'), lambda state: state.has_Pearl() and state.has_misery_mire_medallion()) # sword not required to use medallion for opening in swordless (!)
@ -467,7 +472,7 @@ def swordless_rules(world):
set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.has_key('Small Key (Skull Woods)', 3) and state.has('Fire Rod')) # no curtain set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.has_key('Small Key (Skull Woods)', 3) and state.has('Fire Rod')) # no curtain
set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or state.has('Bombos')) #in swordless mode bombos pads are present in the relevant parts of ice palace set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or state.has('Bombos')) #in swordless mode bombos pads are present in the relevant parts of ice palace
set_rule(world.get_location('Agahnim 2'), lambda state: state.has('Hammer') or state.has('Bug Catching Net')) set_rule(world.get_location('Agahnim 2'), lambda state: state.has('Hammer') or state.has('Bug Catching Net'))
set_rule(world.get_location('Ganon'), lambda state: state.has('Hammer') and state.has_fire_source() and state.has('Silver Arrows') and state.has('Bow') and state.has('Crystal 1') and state.has('Crystal 2') set_rule(world.get_location('Ganon'), lambda state: state.has('Hammer') and state.has_fire_source() and state.has('Silver Arrows') and state.can_shoot_arrows() and state.has('Crystal 1') and state.has('Crystal 2')
and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7')) and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7'))
set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has('Hammer')) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has('Hammer')) # need to damage ganon to get tiles to drop
@ -780,13 +785,8 @@ def set_bunny_rules(world):
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)', bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)',
'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid', 'Spiral Cave (Top)', 'Desert Palace Main (Inner)'] 'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid', 'Spiral Cave (Top)', 'Desert Palace Main (Inner)']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge'] bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins']
if world.get_region('Dam').is_dark_world:
# if Dam is is dark world, then it is required to have the pearl to get the sunken item
add_rule(world.get_location('Sunken Treasure'), lambda state: state.has_Pearl())
# similarly we need perl to get across the swamp palace moat
add_rule(world.get_entrance('Swamp Palace Moat'), lambda state: state.has_Pearl())
def path_to_access_rule(path, entrance): def path_to_access_rule(path, entrance):
return lambda state: state.can_reach(entrance) and all(rule(state) for rule in path) return lambda state: state.can_reach(entrance) and all(rule(state) for rule in path)
@ -805,7 +805,7 @@ def set_bunny_rules(world):
# We will search entrances recursively until we find # We will search entrances recursively until we find
# one that leads to an exclusively light world region # one that leads to an exclusively light world region
# for each such entrance a new option ios aded that consist of: # for each such entrance a new option is added that consist of:
# a) being able to reach it, and # a) being able to reach it, and
# b) being able to access all entrances from there to `region` # b) being able to access all entrances from there to `region`
seen = set([region]) seen = set([region])
@ -836,6 +836,10 @@ def set_bunny_rules(world):
for exit in region.exits: for exit in region.exits:
add_rule(exit, rule) add_rule(exit, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop')
if paradox_shop.is_dark_world:
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
# Add requirements for all locations that are actually in the dark world, except those available to the bunny # Add requirements for all locations that are actually in the dark world, except those available to the bunny
for location in world.get_locations(): for location in world.get_locations():
if location.parent_region.is_dark_world: if location.parent_region.is_dark_world:

462
Text.py
View File

@ -437,9 +437,10 @@ class SceneLargeCreditLine(SceneCreditLine):
buf += LargeCreditBottomMapper.convert(self.text) buf += LargeCreditBottomMapper.convert(self.text)
return buf return buf
class MultiByteTextMapper(object):
def string_to_alttp_text(s, maxbytes=256): @classmethod
outbuf = string_to_alttp_core(s) def convert(cls, text, maxbytes=256):
outbuf = MultiByteCoreTextMapper.convert(text)
# check for max length # check for max length
if len(outbuf) > maxbytes - 2: if len(outbuf) > maxbytes - 2:
@ -452,6 +453,7 @@ def string_to_alttp_text(s, maxbytes=256):
outbuf.append(0x7F) outbuf.append(0x7F)
return outbuf return outbuf
class MultiByteCoreTextMapper(object):
special_commands = { special_commands = {
"{SPEED0}": [0x7A, 0x00], "{SPEED0}": [0x7A, 0x00],
"{SPEED2}": [0x7A, 0x02], "{SPEED2}": [0x7A, 0x02],
@ -477,18 +479,19 @@ special_commands = {
"{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03], "{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03],
} }
def string_to_alttp_core(s, pause=True, wrap=14): @classmethod
s = s.upper() def convert(cls, text, pause=True, wrap=14):
lines = s.split('\n') text = text.upper()
lines = text.split('\n')
outbuf = bytearray() outbuf = bytearray()
lineindex = 0 lineindex = 0
is_intro = '{INTRO}' in s is_intro = '{INTRO}' in text
while lines: while lines:
linespace = wrap linespace = wrap
line = lines.pop(0) line = lines.pop(0)
if line.startswith('{'): if line.startswith('{'):
outbuf.extend(special_commands[line]) outbuf.extend(cls.special_commands[line])
continue continue
words = line.split(' ') words = line.split(' ')
@ -497,21 +500,21 @@ def string_to_alttp_core(s, pause=True, wrap=14):
while words: while words:
word = words.pop(0) word = words.pop(0)
# sanity check: if the word we have is more than 14 characters, we take as much as we can still fit and push the rest back for later # sanity check: if the word we have is more than 14 characters, we take as much as we can still fit and push the rest back for later
if wordlen(word) > wrap: if cls.wordlen(word) > wrap:
if linespace < wrap: if linespace < wrap:
word = ' ' + word word = ' ' + word
(word_first, word_rest) = splitword(word, linespace) (word_first, word_rest) = cls.splitword(word, linespace)
words.insert(0, word_rest) words.insert(0, word_rest)
lines.insert(0, ' '.join(words)) lines.insert(0, ' '.join(words))
write_word(outbuf, word_first) outbuf.extend(RawMBTextMapper.convert(word_first))
break break
if wordlen(word) <= (linespace if linespace == wrap else linespace - 1): if cls.wordlen(word) <= (linespace if linespace == wrap else linespace - 1):
if linespace < wrap: if linespace < wrap:
word = ' ' + word word = ' ' + word
linespace -= wordlen(word) linespace -= cls.wordlen(word)
write_word(outbuf, word) outbuf.extend(RawMBTextMapper.convert(word))
else: else:
# ran out of space, push word and lines back and continue with next line # ran out of space, push word and lines back and continue with next line
words.insert(0, word) words.insert(0, word)
@ -530,6 +533,39 @@ def string_to_alttp_core(s, pause=True, wrap=14):
outbuf.append(0x73) outbuf.append(0x73)
return outbuf return outbuf
@classmethod
def wordlen(cls, word):
l = 0
offset = 0
while offset < len(word):
c_len, offset = cls.charlen(word, offset)
l += c_len
return l
@classmethod
def splitword(cls, word, length):
l = 0
offset = 0
while True:
c_len, new_offset = cls.charlen(word, offset)
if l+c_len > length:
break
l += c_len
offset = new_offset
return (word[0:offset], word[offset:])
@classmethod
def charlen(cls, word, offset):
c = word[offset]
if c in ['>', '¼', '½', '']:
return (2, offset+1)
if c in ['@']:
return (4, offset+1)
if c in ['', '', '', '']:
return (2, offset+1)
return (1, offset+1)
class CompressedTextMapper(object):
two_byte_commands = [ two_byte_commands = [
0x6B, 0x6C, 0x6D, 0x6E, 0x6B, 0x6C, 0x6D, 0x6E,
0x77, 0x78, 0x79, 0x7A 0x77, 0x78, 0x79, 0x7A
@ -543,8 +579,9 @@ specially_coded_commands = {
0x7A: 0xFC, 0x7A: 0xFC,
} }
def string_to_alttp_text_compressed(s, pause=True, max_bytes_expanded=0x800, wrap=14): @classmethod
inbuf = string_to_alttp_core(s, pause, wrap) def convert(cls, text, pause=True, max_bytes_expanded=0x800, wrap=14):
inbuf = MultiByteCoreTextMapper.convert(text, pause, wrap)
# Links name will need 8 bytes in the target buffer # Links name will need 8 bytes in the target buffer
# and two will be used by the terminator # and two will be used by the terminator
@ -565,62 +602,47 @@ def string_to_alttp_text_compressed(s, pause=True, max_bytes_expanded=0x800, wra
outbuf.append(0xFD) outbuf.append(0xFD)
outbuf.append(inbuf.pop()) outbuf.append(inbuf.pop())
elif val >= 0x67: elif val >= 0x67:
if val in specially_coded_commands: if val in cls.specially_coded_commands:
outbuf.append(specially_coded_commands[val]) outbuf.append(cls.specially_coded_commands[val])
else: else:
outbuf.append(0xFE) outbuf.append(0xFE)
outbuf.append(val) outbuf.append(val)
if val in two_byte_commands: if val in cls.two_byte_commands:
outbuf.append(inbuf.pop()) outbuf.append(inbuf.pop())
else: else:
raise ValueError("Unexpected byte found in uncompressed string") raise ValueError("Unexpected byte found in uncompressed string")
return outbuf return outbuf
class CharTextMapper(object):
number_offset = None
alpha_offset = 0
char_map = {}
@classmethod
def map_char(cls, char):
if cls.number_offset is not None:
if 0x30 <= ord(char) <= 0x39:
return ord(char) + cls.number_offset
if 0x61 <= ord(char) <= 0x7A:
return ord(char) + cls.alpha_offset
return cls.char_map.get(char, cls.char_map[' '])
@classmethod
def convert(cls, text):
buf = bytearray()
for char in text.lower():
buf.append(cls.map_char(char))
return buf
def wordlen(word): class RawMBTextMapper(CharTextMapper):
l = 0
offset = 0
while offset < len(word):
c_len, offset = charlen(word, offset)
l += c_len
return l
def splitword(word, length):
l = 0
offset = 0
while True:
c_len, new_offset = charlen(word, offset)
if l+c_len > length:
break
l += c_len
offset = new_offset
return (word[0:offset], word[offset:])
def charlen(word, offset):
c = word[offset]
if c in ['>', '¼', '½', '']:
return (2, offset+1)
if c in ['@']:
return (4, offset+1)
if c in ['', '', '', '']:
return (2, offset+1)
return (1, offset+1)
def write_word(buf, word):
for char in word:
res = char_to_alttp_char(char)
if isinstance(res, int):
buf.extend([0x00, res])
else:
buf.extend(res)
char_map = {' ': 0xFF, char_map = {' ': 0xFF,
'': 0xC4,
'': 0xC5,
'?': 0xC6, '?': 0xC6,
'!': 0xC7, '!': 0xC7,
',': 0xC8, ',': 0xC8,
'-': 0xC9, '-': 0xC9,
"🡄": 0xCA,
"🡆": 0xCB,
'': 0xCC, '': 0xCC,
'.': 0xCD, '.': 0xCD,
'~': 0xCE, '~': 0xCE,
@ -805,74 +827,338 @@ char_map = {' ': 0xFF,
'': 0x9E, '': 0x9E,
'': 0x9F} '': 0x9F}
kanji = {"": 0x00,
"": 0x01,
"": 0x02,
"": 0x03,
"": 0x04,
"": 0x05,
"": 0x06,
"": 0x07,
"": 0x08,
"": 0x09,
"": 0x0A,
"": 0x0B,
"": 0x0C,
"": 0x0D,
"": 0x0E,
"": 0x0F,
"": 0x10,
"": 0x11,
"": 0x12,
"": 0x13,
"": 0x14,
"": 0x15,
"": 0x16,
"": 0x17,
"": 0x18,
"": 0x19,
"": 0x1A,
"": 0x1B,
"": 0x1C,
"": 0x1D,
"": 0x1E,
"": 0x1F,
"": 0x20,
"": 0x21,
"": 0x22,
"": 0x23,
"": 0x24,
"": 0x25,
"": 0x26,
"": 0x27,
"": 0x28,
"": 0x29,
"": 0x2A,
"": 0x2B,
"退": 0x2C,
"": 0x2D,
"": 0x2E,
"": 0x2F,
"": 0x30,
"": 0x31,
"": 0x32,
"": 0x33,
"": 0x34,
"": 0x35,
"": 0x36,
"": 0x37,
"": 0x38,
"": 0x39,
"": 0x3A,
"": 0x3B,
"": 0x3C,
"": 0x3D,
"": 0x3E,
"": 0x3F,
"": 0x40,
"": 0x41,
"": 0x42,
"": 0x43,
"": 0x44,
"": 0x45,
"": 0x46,
"": 0x47,
"": 0x48,
"": 0x49,
"": 0x4A,
"": 0x4B,
"": 0x4C,
"": 0x4D,
"": 0x4E,
"": 0x4F,
"": 0x50,
"": 0x51,
"": 0x52,
"": 0x53,
"": 0x54,
"": 0x55,
"": 0x56,
"": 0x57,
"": 0x58,
"": 0x59,
"": 0x5A,
"": 0x5B,
"": 0x5C,
"": 0x5D,
"": 0x5E,
"": 0x5F,
"": 0x60,
"": 0x61,
"": 0x62,
"": 0x63,
"": 0x64,
"": 0x65,
"": 0x66,
"": 0x67,
"": 0x68,
"": 0x69,
"": 0x6A,
"": 0x6B,
"": 0x6C,
"": 0x6D,
"": 0x6E,
"": 0x6F,
"": 0x70,
"": 0x71,
"": 0x72,
"": 0x73,
"": 0x74,
"": 0x75,
"": 0x76,
"": 0x77,
"": 0x78,
"": 0x79,
"": 0x7A,
"": 0x7B,
"": 0x7C,
"": 0x7D,
"": 0x7E,
"": 0x7F,
"": 0x80,
"": 0x81,
"": 0x82,
"": 0x83,
"": 0x84,
"": 0x85,
"": 0x86,
"": 0x87,
"": 0x88,
"使": 0x89,
"": 0x8A,
"": 0x8B,
"": 0x8C,
"": 0x8D,
"": 0x8E,
"": 0x8F,
"殿": 0x90,
"": 0x91,
"": 0x92,
"": 0x93,
"": 0x94,
"": 0x95,
"": 0x96,
"": 0x97,
"": 0x98,
"": 0x99,
"": 0x9A,
"": 0x9B,
"": 0x9C,
"": 0x9D,
"": 0x9E,
"": 0x9F,
"": 0xA0,
"": 0xA1,
"姿": 0xA2,
"": 0xA3,
"": 0xA4,
"": 0xA5,
"": 0xA6,
"": 0xA7,
"": 0xA8,
"": 0xA9,
"": 0xAA,
"": 0xAB,
"": 0xAC,
"": 0xAD,
"": 0xAE,
"": 0xAF,
"": 0xB0,
"": 0xB1,
"": 0xB2,
"": 0xB3,
"": 0xB4,
"": 0xB5,
"": 0xB6,
"": 0xB7,
"": 0xB8,
"": 0xB9,
"": 0xBA,
"": 0xBB,
"": 0xBC,
"": 0xBD,
"": 0xBE,
"": 0xBF,
"": 0xC0,
"": 0xC1,
"": 0xC2,
"": 0xC3,
"": 0xC4,
"": 0xC5,
"": 0xC6,
"": 0xC7,
"": 0xC8,
"": 0xC9,
"": 0xCA,
"": 0xCB,
"": 0xCC,
"": 0xCD,
"": 0xCE,
"": 0xCF,
"": 0xD0,
"": 0xD1,
"": 0xD2,
"": 0xD3,
"": 0xD4,
"": 0xD5,
"": 0xD6,
"": 0xD7,
"": 0xD8,
"": 0xD9,
"": 0xDA,
"": 0xDB,
"": 0xDC,
"": 0xDD,
"": 0xDE,
"": 0xDF,
"": 0xE0,
"": 0xE1,
"": 0xE2,
"": 0xE3,
"": 0xE4,
"": 0xE5,
"": 0xE6,
"": 0xE7,
"": 0xE8,
"": 0xE9,
"": 0xEA,
"": 0xEB,
"": 0xEC,
"": 0xED,
"": 0xEE,
"": 0xEF,
"": 0xF0,
"": 0xF1,
"": 0xF2,
"": 0xF3,
#"力": 0xF4,
"": 0xF5,
"": 0xF6,
"": 0xF7,
"": 0xF8,
"": 0xF9,
"": 0xFA,
"": 0xFB,
"": 0xFC,
"": 0xFD,
"": 0xFE,
"": 0xFF}
alpha_offset = 0x69
number_offset = 0x70
def char_to_alttp_char(char):
if 0x30 <= ord(char) <= 0x39:
return ord(char) + 0x70
if 0x41 <= ord(char) <= 0x5A:
return ord(char) + 0x69
return char_map.get(char, 0xFF)
class TextMapper(object):
number_offset = None
alpha_offset = 0
char_map = {}
@classmethod @classmethod
def map_char(cls, char): def map_char(cls, char):
if cls.number_offset is not None: if char in cls.kanji:
if 0x30 <= ord(char) <= 0x39: return [0x01, cls.kanji[char]]
return ord(char) + cls.number_offset return super().map_char(char)
if 0x61 <= ord(char) <= 0x7A:
return ord(char) + cls.alpha_offset
return cls.char_map.get(char, cls.char_map[' '])
@classmethod @classmethod
def convert(cls, text): def convert(cls, text):
buf = bytearray() buf = bytearray()
for char in text.lower(): for char in text.lower():
buf.append(cls.map_char(char)) res = cls.map_char(char)
if isinstance(res, int):
buf.extend([0x00, res])
else:
buf.extend(res)
return buf return buf
class GoldCreditMapper(TextMapper): class GoldCreditMapper(CharTextMapper):
char_map = {' ': 0x9F, char_map = {' ': 0x9F,
',': 0x34, ',': 0x34,
'.': 0x37, "'": 0x35,
'-': 0x36, '-': 0x36,
"'": 0x35} '.': 0x37,}
alpha_offset = -0x47 alpha_offset = -0x47
class GreenCreditMapper(TextMapper): class GreenCreditMapper(CharTextMapper):
char_map = {' ': 0x9F, char_map = {' ': 0x9F,
'.': 0x52} '·': 0x52}
alpha_offset = -0x29 alpha_offset = -0x29
class RedCreditMapper(TextMapper): class RedCreditMapper(CharTextMapper):
char_map = {' ': 0x9F} #fixme char_map = {' ': 0x9F}
alpha_offset = -0x61 alpha_offset = -0x61
class LargeCreditTopMapper(TextMapper): class LargeCreditTopMapper(CharTextMapper):
char_map = {' ': 0x9F, char_map = {' ': 0x9F,
"'": 0x77, "'": 0x77,
'!': 0x78, '!': 0x78,
'.': 0xA0, '.': 0xA0,
'#': 0xA1, '#': 0xA1,
'/': 0xA2, '/': 0xA2,
':': 0xA3} ':': 0xA3,
',': 0xA4,
'?': 0xA5,
'=': 0xA6,
'"': 0xA7,
'-': 0xA8,
'·': 0xA9,
'': 0xA9,
'': 0xAA,
'': 0xAB,}
alpha_offset = -0x04 alpha_offset = -0x04
number_offset = 0x23 number_offset = 0x23
class LargeCreditBottomMapper(TextMapper): class LargeCreditBottomMapper(CharTextMapper):
char_map = {' ': 0x9F, char_map = {' ': 0x9F,
"'": 0x9D, "'": 0x9D,
'!': 0x9E, '!': 0x9E,
'.': 0xC0, '.': 0xC0,
'#': 0xC1, '#': 0xC1,
'/': 0xC2, '/': 0xC2,
':': 0xC3} ':': 0xC3,
',': 0xC4,
'?': 0xC5,
'=': 0xC6,
'"': 0xC7,
'-': 0xC8,
'·': 0xC9,
'': 0xC9,
'': 0xCA,
'': 0xCB,}
alpha_offset = 0x22 alpha_offset = 0x22
number_offset = 0x49 number_offset = 0x49

View File

@ -2,6 +2,14 @@ import os
import subprocess import subprocess
import sys import sys
def int16_as_bytes(value):
value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF]
def int32_as_bytes(value):
value = value & 0xFFFFFFFF
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
def is_bundled(): def is_bundled():
return getattr(sys, 'frozen', False) return getattr(sys, 'frozen', False)

File diff suppressed because one or more lines are too long