2020-04-22 03:09:46 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-04-28 03:59:03 +00:00
|
|
|
__version__ = "2.1.2"
|
2020-04-20 12:50:49 +00:00
|
|
|
_version_tuple = tuple(int(piece, 10) for piece in __version__.split("."))
|
|
|
|
|
2017-11-28 14:36:32 +00:00
|
|
|
import os
|
2017-12-17 05:25:46 +00:00
|
|
|
import subprocess
|
2017-11-28 14:36:32 +00:00
|
|
|
import sys
|
2020-02-16 14:32:40 +00:00
|
|
|
import typing
|
|
|
|
import functools
|
|
|
|
|
2020-04-24 03:29:02 +00:00
|
|
|
from yaml import load, dump
|
2020-02-16 14:32:40 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
from yaml import CLoader as Loader
|
|
|
|
except ImportError:
|
|
|
|
from yaml import Loader
|
|
|
|
|
2017-11-28 14:36:32 +00:00
|
|
|
|
2018-02-17 23:38:54 +00:00
|
|
|
def int16_as_bytes(value):
|
|
|
|
value = value & 0xFFFF
|
|
|
|
return [value & 0xFF, (value >> 8) & 0xFF]
|
|
|
|
|
2020-02-16 14:32:40 +00:00
|
|
|
|
2018-02-17 23:38:54 +00:00
|
|
|
def int32_as_bytes(value):
|
|
|
|
value = value & 0xFFFFFFFF
|
|
|
|
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
|
|
|
|
|
2020-02-16 14:32:40 +00:00
|
|
|
|
2018-09-23 02:51:54 +00:00
|
|
|
def pc_to_snes(value):
|
|
|
|
return ((value<<1) & 0x7F0000)|(value & 0x7FFF)|0x8000
|
|
|
|
|
|
|
|
def snes_to_pc(value):
|
|
|
|
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
|
|
|
|
|
2020-01-14 09:42:27 +00:00
|
|
|
def parse_player_names(names, players, teams):
|
2020-03-02 22:27:16 +00:00
|
|
|
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
2020-01-14 09:42:27 +00:00
|
|
|
ret = []
|
|
|
|
while names or len(ret) < teams:
|
|
|
|
team = [n[:16] for n in names[:players]]
|
2020-02-23 16:06:44 +00:00
|
|
|
# where does the 16 character limit come from?
|
2020-01-14 09:42:27 +00:00
|
|
|
while len(team) != players:
|
2020-04-18 19:46:57 +00:00
|
|
|
team.append(f"Player{len(team) + 1}")
|
2020-01-14 09:42:27 +00:00
|
|
|
ret.append(team)
|
|
|
|
|
|
|
|
names = names[players:]
|
|
|
|
return ret
|
|
|
|
|
2017-11-28 14:36:32 +00:00
|
|
|
def is_bundled():
|
|
|
|
return getattr(sys, 'frozen', False)
|
|
|
|
|
|
|
|
def local_path(path):
|
2020-03-15 18:32:00 +00:00
|
|
|
if local_path.cached_path:
|
2017-11-28 14:36:32 +00:00
|
|
|
return os.path.join(local_path.cached_path, path)
|
|
|
|
|
2020-03-23 06:45:40 +00:00
|
|
|
elif is_bundled():
|
|
|
|
if hasattr(sys, "_MEIPASS"):
|
|
|
|
# we are running in a PyInstaller bundle
|
|
|
|
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
|
|
|
|
else:
|
|
|
|
# cx_Freeze
|
|
|
|
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
2017-11-28 14:36:32 +00:00
|
|
|
else:
|
|
|
|
# we are running in a normal Python environment
|
2020-03-23 06:45:40 +00:00
|
|
|
import __main__
|
|
|
|
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
2020-03-15 18:32:00 +00:00
|
|
|
|
2017-11-28 14:36:32 +00:00
|
|
|
return os.path.join(local_path.cached_path, path)
|
|
|
|
|
|
|
|
local_path.cached_path = None
|
|
|
|
|
|
|
|
def output_path(path):
|
2020-03-15 18:32:00 +00:00
|
|
|
if output_path.cached_path:
|
2017-11-28 14:36:32 +00:00
|
|
|
return os.path.join(output_path.cached_path, path)
|
|
|
|
|
2020-03-15 18:32:00 +00:00
|
|
|
if not is_bundled() and not hasattr(sys, "_MEIPASS"):
|
|
|
|
# this should trigger if it's cx_freeze bundling
|
2017-11-28 14:36:32 +00:00
|
|
|
output_path.cached_path = '.'
|
|
|
|
return os.path.join(output_path.cached_path, path)
|
|
|
|
else:
|
2020-03-15 18:32:00 +00:00
|
|
|
# has been PyInstaller packaged, so cannot use CWD for output.
|
2017-11-28 14:36:32 +00:00
|
|
|
if sys.platform == 'win32':
|
2020-03-15 18:32:00 +00:00
|
|
|
# windows
|
2017-11-28 14:36:32 +00:00
|
|
|
import ctypes.wintypes
|
2020-03-15 18:32:00 +00:00
|
|
|
CSIDL_PERSONAL = 5 # My Documents
|
|
|
|
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
|
2017-11-28 14:36:32 +00:00
|
|
|
|
|
|
|
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
|
|
|
|
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
|
|
|
|
|
|
|
|
documents = buf.value
|
|
|
|
|
|
|
|
elif sys.platform == 'darwin':
|
2017-12-17 05:25:46 +00:00
|
|
|
from AppKit import NSSearchPathForDirectoriesInDomains # pylint: disable=import-error
|
2017-11-28 14:36:32 +00:00
|
|
|
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
|
|
|
|
NSDocumentDirectory = 9
|
|
|
|
NSUserDomainMask = 1
|
|
|
|
# True for expanding the tilde into a fully qualified path
|
|
|
|
documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0]
|
|
|
|
else:
|
|
|
|
raise NotImplementedError('Not supported yet')
|
|
|
|
|
|
|
|
output_path.cached_path = os.path.join(documents, 'ALttPEntranceRandomizer')
|
|
|
|
if not os.path.exists(output_path.cached_path):
|
|
|
|
os.mkdir(output_path.cached_path)
|
|
|
|
return os.path.join(output_path.cached_path, path)
|
|
|
|
|
|
|
|
output_path.cached_path = None
|
|
|
|
|
|
|
|
def open_file(filename):
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
os.startfile(filename)
|
|
|
|
else:
|
2017-12-17 05:25:46 +00:00
|
|
|
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
|
2017-11-28 14:36:32 +00:00
|
|
|
subprocess.call([open_command, filename])
|
2017-12-02 14:21:04 +00:00
|
|
|
|
|
|
|
def close_console():
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
#windows
|
|
|
|
import ctypes.wintypes
|
|
|
|
try:
|
|
|
|
ctypes.windll.kernel32.FreeConsole()
|
2017-12-17 05:25:46 +00:00
|
|
|
except Exception:
|
2017-12-02 14:21:04 +00:00
|
|
|
pass
|
2018-01-01 19:42:23 +00:00
|
|
|
|
|
|
|
def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', new_rom='working.sfc'):
|
|
|
|
from collections import OrderedDict
|
|
|
|
import json
|
|
|
|
import hashlib
|
|
|
|
with open(old_rom, 'rb') as stream:
|
|
|
|
old_rom_data = bytearray(stream.read())
|
|
|
|
with open(new_rom, 'rb') as stream:
|
|
|
|
new_rom_data = bytearray(stream.read())
|
|
|
|
# extend to 2 mb
|
2020-01-22 16:50:00 +00:00
|
|
|
old_rom_data.extend(bytearray([0x00]) * (2097152 - len(old_rom_data)))
|
2018-01-01 19:42:23 +00:00
|
|
|
|
|
|
|
out_data = OrderedDict()
|
|
|
|
for idx, old in enumerate(old_rom_data):
|
|
|
|
new = new_rom_data[idx]
|
|
|
|
if old != new:
|
|
|
|
out_data[idx] = [int(new)]
|
|
|
|
for offset in reversed(list(out_data.keys())):
|
|
|
|
if offset - 1 in out_data:
|
|
|
|
out_data[offset-1].extend(out_data.pop(offset))
|
|
|
|
with open('data/base2current.json', 'wt') as outfile:
|
2020-02-16 14:32:40 +00:00
|
|
|
json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":"))
|
2018-01-01 19:42:23 +00:00
|
|
|
|
|
|
|
basemd5 = hashlib.md5()
|
|
|
|
basemd5.update(new_rom_data)
|
|
|
|
return "New Rom Hash: " + basemd5.hexdigest()
|
2020-02-09 04:28:48 +00:00
|
|
|
|
|
|
|
|
2020-02-16 14:32:40 +00:00
|
|
|
parse_yaml = functools.partial(load, Loader=Loader)
|
|
|
|
|
2020-02-09 04:28:48 +00:00
|
|
|
|
2020-02-16 14:32:40 +00:00
|
|
|
class Hint(typing.NamedTuple):
|
|
|
|
receiving_player: int
|
|
|
|
finding_player: int
|
|
|
|
location: int
|
|
|
|
item: int
|
|
|
|
found: bool
|
2020-03-05 23:48:23 +00:00
|
|
|
|
2020-04-22 03:09:46 +00:00
|
|
|
def re_check(self, ctx, team) -> Hint:
|
|
|
|
if self.found:
|
|
|
|
return self
|
|
|
|
found = self.location in ctx.location_checks[team, self.finding_player]
|
|
|
|
if found:
|
|
|
|
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found)
|
|
|
|
return self
|
|
|
|
|
2020-04-22 13:50:14 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash((self.receiving_player, self.finding_player, self.location, self.item))
|
|
|
|
|
2020-03-05 23:48:23 +00:00
|
|
|
def get_public_ipv4() -> str:
|
|
|
|
import socket
|
|
|
|
import urllib.request
|
|
|
|
import logging
|
|
|
|
ip = socket.gethostbyname(socket.gethostname())
|
|
|
|
try:
|
|
|
|
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
|
|
|
|
except Exception as e:
|
|
|
|
try:
|
|
|
|
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
|
|
|
|
except:
|
|
|
|
logging.exception(e)
|
|
|
|
pass # we could be offline, in a local game, so no point in erroring out
|
|
|
|
return ip
|
2020-03-15 18:32:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_options() -> dict:
|
2020-03-23 06:59:55 +00:00
|
|
|
if not hasattr(get_options, "options"):
|
2020-03-15 18:32:00 +00:00
|
|
|
locations = ("options.yaml", "host.yaml",
|
|
|
|
local_path("options.yaml"), local_path("host.yaml"))
|
|
|
|
|
|
|
|
for location in locations:
|
|
|
|
if os.path.exists(location):
|
|
|
|
with open(location) as f:
|
2020-03-23 06:59:55 +00:00
|
|
|
get_options.options = parse_yaml(f.read())
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise FileNotFoundError(f"Could not find {locations[1]} to load options.")
|
|
|
|
return get_options.options
|
2020-04-14 18:22:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_item_name_from_id(code):
|
|
|
|
import Items
|
|
|
|
return Items.lookup_id_to_name.get(code, f'Unknown item (ID:{code})')
|
|
|
|
|
|
|
|
|
|
|
|
def get_location_name_from_address(address):
|
|
|
|
import Regions
|
|
|
|
return Regions.lookup_id_to_name.get(address, f'Unknown location (ID:{address})')
|
|
|
|
|
|
|
|
|
2020-04-24 03:29:02 +00:00
|
|
|
def persistent_store(category, key, value):
|
|
|
|
path = local_path("_persistent_storage.yaml")
|
|
|
|
storage: dict = persistent_load()
|
|
|
|
category = storage.setdefault(category, {})
|
|
|
|
category[key] = value
|
|
|
|
with open(path, "wt") as f:
|
|
|
|
f.write(dump(storage))
|
|
|
|
|
|
|
|
|
2020-04-26 13:14:30 +00:00
|
|
|
def persistent_load() -> typing.Dict[dict]:
|
2020-04-24 03:29:02 +00:00
|
|
|
path = local_path("_persistent_storage.yaml")
|
|
|
|
storage: dict = {}
|
|
|
|
if os.path.exists(path):
|
|
|
|
try:
|
|
|
|
with open(path, "r") as f:
|
|
|
|
storage = parse_yaml(f.read())
|
|
|
|
except Exception as e:
|
|
|
|
import logging
|
|
|
|
logging.debug(f"Could not read store: {e}")
|
|
|
|
return storage
|
|
|
|
|
|
|
|
|
2020-04-14 18:22:42 +00:00
|
|
|
class ReceivedItem(typing.NamedTuple):
|
|
|
|
item: int
|
|
|
|
location: int
|
|
|
|
player: int
|