--remote_items toggle: items will no longer be stored in the rom, a connection to a multiworld server will be required to receive them
This commit is contained in:
parent
2d26d63cce
commit
f988f7671b
|
@ -60,6 +60,7 @@ class World(object):
|
|||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('_region_cache', {})
|
||||
set_player_attr('player_names', [])
|
||||
set_player_attr('remote_items', False)
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
set_player_attr('powder_patch_required', False)
|
||||
|
|
|
@ -266,6 +266,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
|
||||
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||
parser.add_argument('--remote_items', default=defval(False), action='store_true')
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
|
@ -291,7 +292,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
|
||||
'remote_items']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
|
|
12
Main.py
12
Main.py
|
@ -39,6 +39,7 @@ def main(args, seed=None):
|
|||
world.seed = int(seed)
|
||||
random.seed(world.seed)
|
||||
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
|
@ -187,6 +188,7 @@ def main(args, seed=None):
|
|||
outfilepname = f'_T{team+1}' if world.teams > 1 else ''
|
||||
if world.players > 1:
|
||||
outfilepname += f'_P{player}'
|
||||
if world.players > 1 or world.teams > 1:
|
||||
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
|
||||
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
|
||||
world.mode[player], world.goal[player],
|
||||
|
@ -197,9 +199,12 @@ def main(args, seed=None):
|
|||
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
|
||||
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
|
||||
|
||||
multidata = zlib.compress(json.dumps((parsed_names, rom_names,
|
||||
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
|
||||
).encode("utf-8"))
|
||||
multidata = zlib.compress(json.dumps({"names": parsed_names,
|
||||
"roms": rom_names,
|
||||
"remote_items": [player for player in range(1, world.players + 1) if world.remote_items[player]],
|
||||
"locations": [((location.address, location.player), (location.item.code, location.item.player))
|
||||
for location in world.get_filled_locations() if type(location.address) is int]
|
||||
}).encode("utf-8"))
|
||||
if args.jsonout:
|
||||
jsonout["multidata"] = list(multidata)
|
||||
else:
|
||||
|
@ -228,6 +233,7 @@ def copy_world(world):
|
|||
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
ret.teams = world.teams
|
||||
ret.player_names = copy.deepcopy(world.player_names)
|
||||
ret.remote_items = world.remote_items.copy()
|
||||
ret.required_medallions = world.required_medallions.copy()
|
||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||
|
|
|
@ -68,7 +68,9 @@ class Context:
|
|||
self.slot = None
|
||||
self.player_names = {}
|
||||
self.locations_checked = set()
|
||||
self.locations_scouted = set()
|
||||
self.items_received = []
|
||||
self.locations_info = {}
|
||||
self.awaiting_rom = False
|
||||
self.rom = None
|
||||
self.auth = None
|
||||
|
@ -97,11 +99,15 @@ INGAME_MODES = {0x07, 0x09, 0x0b}
|
|||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
|
||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
|
||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
||||
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
|
||||
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
|
||||
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||
|
||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
"Blind's Hideout - Left": (0x11d, 0x20),
|
||||
|
@ -595,6 +601,7 @@ async def server_loop(ctx : Context, address = None):
|
|||
ctx.awaiting_rom = False
|
||||
ctx.auth = None
|
||||
ctx.items_received = []
|
||||
ctx.locations_info = {}
|
||||
socket, ctx.socket = ctx.socket, None
|
||||
if socket is not None and not socket.closed:
|
||||
await socket.close()
|
||||
|
@ -643,8 +650,13 @@ async def process_server_cmd(ctx : Context, cmd, args):
|
|||
if cmd == 'Connected':
|
||||
ctx.team, ctx.slot = args[0]
|
||||
ctx.player_names = {p: n for p, n in args[1]}
|
||||
msgs = []
|
||||
if ctx.locations_checked:
|
||||
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
|
||||
msgs.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
|
||||
if ctx.locations_scouted:
|
||||
msgs.append(['LocationScouts', list(ctx.locations_scouted)])
|
||||
if msgs:
|
||||
await send_msgs(ctx.socket, msgs)
|
||||
|
||||
if cmd == 'ReceivedItems':
|
||||
start_index, items = args
|
||||
|
@ -659,6 +671,14 @@ async def process_server_cmd(ctx : Context, cmd, args):
|
|||
for item in items:
|
||||
ctx.items_received.append(ReceivedItem(*item))
|
||||
|
||||
if cmd == 'LocationInfo':
|
||||
for location, item, player in args:
|
||||
if location not in ctx.locations_info:
|
||||
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
|
||||
item_name = replacements.get(item, get_item_name_from_id(item))
|
||||
print(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}")
|
||||
ctx.locations_info[location] = (item, player)
|
||||
|
||||
if cmd == 'ItemSent':
|
||||
player_sent, location, player_recvd, item = args
|
||||
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
|
||||
|
@ -840,6 +860,7 @@ async def game_watcher(ctx : Context):
|
|||
|
||||
ctx.rom = list(rom)
|
||||
ctx.locations_checked = set()
|
||||
ctx.locations_scouted = set()
|
||||
if ctx.awaiting_rom:
|
||||
await server_auth(ctx, False)
|
||||
|
||||
|
@ -851,18 +872,24 @@ async def game_watcher(ctx : Context):
|
|||
if gamemode is None or gamemode[0] not in INGAME_MODES:
|
||||
continue
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7)
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2)
|
||||
assert RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2
|
||||
recv_item = data[2]
|
||||
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4)
|
||||
assert ROOMID_ADDR == RECV_PROGRESS_ADDR + 4
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6)
|
||||
assert ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6
|
||||
roomdata = data[6]
|
||||
assert SCOUT_LOCATION_ADDR == RECV_PROGRESS_ADDR + 7
|
||||
scout_location = data[7]
|
||||
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
print(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
|
||||
await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
|
@ -873,7 +900,11 @@ async def game_watcher(ctx : Context):
|
|||
recv_index += 1
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, bytes([ctx.locations_info[scout_location][0]]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, bytes([ctx.locations_info[scout_location][1]]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ class Context:
|
|||
self.disable_save = False
|
||||
self.player_names = {}
|
||||
self.rom_names = {}
|
||||
self.remote_items = set()
|
||||
self.locations = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
@ -162,7 +163,7 @@ def register_location_checks(ctx : Context, team, slot, locations):
|
|||
for location in locations:
|
||||
if (location, slot) in ctx.locations:
|
||||
target_item, target_player = ctx.locations[(location, slot)]
|
||||
if target_player != slot:
|
||||
if target_player != slot or slot in ctx.remote_items:
|
||||
found = False
|
||||
recvd_items = get_received_items(ctx, team, target_player)
|
||||
for recvd_item in recvd_items:
|
||||
|
@ -173,7 +174,7 @@ def register_location_checks(ctx : Context, team, slot, locations):
|
|||
new_item = ReceivedItem(target_item, location, slot)
|
||||
recvd_items.append(new_item)
|
||||
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
|
||||
print('(Team #%d) %s sent %s to %s (%s)' % (team, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||
print('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||
found_items = True
|
||||
send_new_items(ctx)
|
||||
|
||||
|
@ -232,7 +233,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
client.send_index = len(items)
|
||||
await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))])
|
||||
await send_msgs(client.socket, [['ReceivedItems', (0, tuplize_received_items(items))]])
|
||||
|
||||
if cmd == 'LocationChecks':
|
||||
if type(args) is not list:
|
||||
|
@ -240,6 +241,28 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||
return
|
||||
register_location_checks(ctx, client.team, client.slot, args)
|
||||
|
||||
if cmd == 'LocationScouts':
|
||||
if type(args) is not list:
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
|
||||
return
|
||||
locs = []
|
||||
for location in args:
|
||||
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
|
||||
return
|
||||
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
||||
|
||||
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
|
||||
item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
|
||||
if item_type:
|
||||
target_item = replacements.get(item_type[0], target_item)
|
||||
|
||||
locs.append([loc_name, location, target_item, target_player])
|
||||
|
||||
print(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
||||
await send_msgs(client.socket, [['LocationInfo', [l[1:] for l in locs]]])
|
||||
|
||||
if cmd == 'Say':
|
||||
if type(args) is not str or not args.isprintable():
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'Say']])
|
||||
|
@ -338,11 +361,12 @@ async def main():
|
|||
|
||||
with open(ctx.data_filename, 'rb') as f:
|
||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||
for team, names in enumerate(jsonobj[0]):
|
||||
for team, names in enumerate(jsonobj['names']):
|
||||
for player, name in enumerate(names, 1):
|
||||
ctx.player_names[(team, player)] = name
|
||||
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj[1]}
|
||||
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
|
||||
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']}
|
||||
ctx.remote_items = set(jsonobj['remote_items'])
|
||||
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']}
|
||||
except Exception as e:
|
||||
print('Failed to read multiworld data (%s)' % e)
|
||||
return
|
||||
|
|
41
Rom.py
41
Rom.py
|
@ -11,6 +11,7 @@ import subprocess
|
|||
|
||||
from BaseClasses import CollectionState, ShopType, Region, Location
|
||||
from Dungeons import dungeon_music_addresses
|
||||
from Regions import location_table
|
||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
||||
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_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
|
||||
|
@ -495,23 +496,27 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
continue
|
||||
|
||||
if not location.crystal:
|
||||
# Keys in their native dungeon should use the orignal item code for keys
|
||||
if location.parent_region.dungeon:
|
||||
dungeon = location.parent_region.dungeon
|
||||
if location.item is not None and dungeon.is_dungeon_item(location.item):
|
||||
if location.item.bigkey:
|
||||
itemid = 0x32
|
||||
if location.item.smallkey:
|
||||
itemid = 0x24
|
||||
if location.item.map:
|
||||
itemid = 0x33
|
||||
if location.item.compass:
|
||||
itemid = 0x25
|
||||
if location.item and location.item.player != player:
|
||||
if location.player_address is not None:
|
||||
rom.write_byte(location.player_address, location.item.player)
|
||||
else:
|
||||
itemid = 0x5A
|
||||
if location.item is not None:
|
||||
# Keys in their native dungeon should use the orignal item code for keys
|
||||
if location.parent_region.dungeon:
|
||||
if location.parent_region.dungeon.is_dungeon_item(location.item):
|
||||
if location.item.bigkey:
|
||||
itemid = 0x32
|
||||
if location.item.smallkey:
|
||||
itemid = 0x24
|
||||
if location.item.map:
|
||||
itemid = 0x33
|
||||
if location.item.compass:
|
||||
itemid = 0x25
|
||||
if world.remote_items[player]:
|
||||
itemid = list(location_table.keys()).index(location.name) + 1
|
||||
assert itemid < 0x100
|
||||
rom.write_byte(location.player_address, 0xFF)
|
||||
elif location.item.player != player:
|
||||
if location.player_address is not None:
|
||||
rom.write_byte(location.player_address, location.item.player)
|
||||
else:
|
||||
itemid = 0x5A
|
||||
rom.write_byte(location.address, itemid)
|
||||
else:
|
||||
# crystals
|
||||
|
@ -1229,6 +1234,8 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
|
||||
write_strings(rom, world, player, team)
|
||||
|
||||
rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0)
|
||||
|
||||
# set rom name
|
||||
# 21 bytes
|
||||
from Main import __version__
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue