împlement optional hint system (defaults to off)
This commit is contained in:
parent
7c57a7337c
commit
14e24037a5
|
@ -1,5 +1,5 @@
|
|||
__author__ = "Berserker55" # you can find me on the ALTTP Randomizer Discord
|
||||
__version__ = 1.5
|
||||
__version__ = 1.6
|
||||
|
||||
"""
|
||||
This script launches a Multiplayer "Multiworld" Mystery Game
|
||||
|
@ -10,42 +10,9 @@ After generation the server is automatically launched.
|
|||
It is still up to the host to forward the correct port (38281 by default) and distribute the roms to the players.
|
||||
Regular Mystery has to work for this first, such as a ALTTP Base ROM and Enemizer Setup.
|
||||
A guide can be found here: https://docs.google.com/document/d/19FoqUkuyStMqhOq8uGiocskMo1KMjOW4nEeG81xrKoI/edit
|
||||
This script itself should be placed within the Bonta Multiworld folder, that you download in step 1
|
||||
Configuration can be found in host.yaml
|
||||
"""
|
||||
|
||||
####config####
|
||||
#location of your Enemizer CLI, available here: https://github.com/Bonta0/Enemizer/releases
|
||||
enemizer_location:str = "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
|
||||
#Where to place the resulting files
|
||||
outputpath:str = "MultiMystery"
|
||||
|
||||
#automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
|
||||
#does nothing if the name is not found
|
||||
#example: player_name = "Berserker"
|
||||
player_name:str = ""
|
||||
|
||||
#Zip the resulting roms
|
||||
#0 -> Don't
|
||||
#1 -> Create a zip
|
||||
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
|
||||
zip_roms:int = 1
|
||||
|
||||
#create a spoiler file
|
||||
create_spoiler:bool = True
|
||||
|
||||
#create roms as race coms
|
||||
race:bool= False
|
||||
|
||||
#folder from which the player yaml files are pulled from
|
||||
player_files_folder:str = "Players"
|
||||
|
||||
#Version of python to use for Bonta Multiworld. Probably leave this as is, if you don't know what this does.
|
||||
#can be tagged for bitness, for example "3.8-32" would be latest installed 3.8 on 32 bits
|
||||
#special case: None -> use the python which was used to launch this file.
|
||||
py_version:str = None
|
||||
####end of config####
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -57,31 +24,43 @@ def feedback(text:str):
|
|||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not py_version:
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
print(f"{__author__}'s MultiMystery Launcher V{__version__}")
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
print(f"{__author__}'s MultiMystery Launcher V{__version__}")
|
||||
if not os.path.exists(enemizer_location):
|
||||
feedback(f"Enemizer not found at {enemizer_location}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
|
||||
from Utils import parse_yaml
|
||||
|
||||
multi_mystery_options = parse_yaml(open("host.yaml").read())["multi_mystery_options"]
|
||||
output_path = multi_mystery_options["output_path"]
|
||||
enemizer_path = multi_mystery_options["enemizer_path"]
|
||||
player_files_path = multi_mystery_options["player_files_path"]
|
||||
race = multi_mystery_options["race"]
|
||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||
zip_roms = multi_mystery_options["zip_roms"]
|
||||
player_name = multi_mystery_options["player_name"]
|
||||
|
||||
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
if not os.path.exists(enemizer_path):
|
||||
feedback(f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
|
||||
if not os.path.exists("Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"):
|
||||
feedback("Base rom is expected as Zelda no Densetsu - Kamigami no Triforce (Japan).sfc in the Multiworld root folder please place/rename it there.")
|
||||
player_files = []
|
||||
os.makedirs(player_files_folder, exist_ok=True)
|
||||
for file in os.listdir(player_files_folder):
|
||||
os.makedirs(player_files_path, exist_ok=True)
|
||||
for file in os.listdir(player_files_path):
|
||||
if file.lower().endswith(".yaml"):
|
||||
player_files.append(file)
|
||||
print(f"Player {file[:-5]} found.")
|
||||
player_count = len(player_files)
|
||||
if player_count == 0:
|
||||
feedback(f"No player files found. Please put them in a {player_files_folder} folder.")
|
||||
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
|
||||
else:
|
||||
print(player_count, "Players found.")
|
||||
|
||||
player_string = ""
|
||||
for i,file in enumerate(player_files):
|
||||
player_string += f"--p{i+1} {os.path.join(player_files_folder, file)} "
|
||||
player_string += f"--p{i+1} {os.path.join(player_files_path, file)} "
|
||||
|
||||
player_names = list(file[:-5] for file in player_files)
|
||||
|
||||
|
@ -93,8 +72,8 @@ if __name__ == "__main__":
|
|||
basemysterycommand = f"py -{py_version} Mystery.py" #source
|
||||
|
||||
command = f"{basemysterycommand} --multi {len(player_files)} {player_string} " \
|
||||
f"--names {','.join(player_names)} --enemizercli {enemizer_location} " \
|
||||
f"--outputpath {outputpath}" + " --create_spoiler" if create_spoiler else "" + " --race" if race else ""
|
||||
f"--names {','.join(player_names)} --enemizercli {enemizer_path} " \
|
||||
f"--outputpath {output_path}" + " --create_spoiler" if create_spoiler else "" + " --race" if race else ""
|
||||
print(command)
|
||||
import time
|
||||
start = time.perf_counter()
|
||||
|
@ -116,20 +95,20 @@ if __name__ == "__main__":
|
|||
except IndexError:
|
||||
print(f"Could not find Player {player_name}")
|
||||
else:
|
||||
romfilename = os.path.join(outputpath, f"ER_{seedname}_P{index+1}_{player_name}.sfc")
|
||||
romfilename = os.path.join(output_path, f"ER_{seedname}_P{index+1}_{player_name}.sfc")
|
||||
import webbrowser
|
||||
if os.path.exists(romfilename):
|
||||
print(f"Launching ROM file {romfilename}")
|
||||
webbrowser.open(romfilename)
|
||||
|
||||
if zip_roms:
|
||||
zipname = os.path.join(outputpath, f"ER_{seedname}.zip")
|
||||
zipname = os.path.join(output_path, f"ER_{seedname}.zip")
|
||||
print(f"Creating zipfile {zipname}")
|
||||
import zipfile
|
||||
with zipfile.ZipFile(zipname, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for file in os.listdir(outputpath):
|
||||
for file in os.listdir(output_path):
|
||||
if file.endswith(".sfc") and seedname in file:
|
||||
zf.write(os.path.join(outputpath, file), file)
|
||||
zf.write(os.path.join(output_path, file), file)
|
||||
print(f"Packed {file} into zipfile {zipname}")
|
||||
if zip_roms == 2 and player_name.lower() not in file.lower():
|
||||
os.remove(file)
|
||||
|
@ -143,7 +122,7 @@ if __name__ == "__main__":
|
|||
baseservercommand = f"py -{py_version} MultiServer.py" # source
|
||||
#don't have a mac to test that. If you try to run compiled on mac, good luck.
|
||||
|
||||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(outputpath, multidataname)}")
|
||||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
|
108
MultiServer.py
108
MultiServer.py
|
@ -7,6 +7,7 @@ import re
|
|||
import shlex
|
||||
import urllib.request
|
||||
import zlib
|
||||
import collections
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
@ -16,6 +17,7 @@ import aioconsole
|
|||
|
||||
import Items
|
||||
import Regions
|
||||
import Utils
|
||||
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
|
||||
|
||||
class Client:
|
||||
|
@ -28,7 +30,7 @@ class Client:
|
|||
self.send_index = 0
|
||||
|
||||
class Context:
|
||||
def __init__(self, host, port, password):
|
||||
def __init__(self, host:str, port:int, password:str, location_check_points:int, hint_cost:int):
|
||||
self.data_filename = None
|
||||
self.save_filename = None
|
||||
self.disable_save = False
|
||||
|
@ -43,6 +45,29 @@ class Context:
|
|||
self.countdown_timer = 0
|
||||
self.clients = []
|
||||
self.received_items = {}
|
||||
self.location_checks = collections.defaultdict(lambda: 0)
|
||||
self.hint_cost = hint_cost
|
||||
self.location_check_points = location_check_points
|
||||
self.hints_used = collections.defaultdict(lambda: 0)
|
||||
|
||||
def get_save(self) -> dict:
|
||||
return {
|
||||
"rom_names": list(self.rom_names.items()),
|
||||
"received_items": tuple((k, [i.__dict__ for i in v]) for k, v in self.received_items.items()),
|
||||
"hints_used" : tuple((key,value) for key, value in self.hints_used.items()),
|
||||
"location_checks" : tuple((key,value) for key, value in self.location_checks.items())
|
||||
}
|
||||
|
||||
def set_save(self, savedata: dict):
|
||||
rom_names = savedata["rom_names"]
|
||||
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in savedata["received_items"]}
|
||||
if not all([self.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
|
||||
raise Exception('Save file mismatch, will start a new game')
|
||||
self.received_items = received_items
|
||||
self.hints_used.update({tuple(key): value for key, value in savedata["hints_used"]})
|
||||
self.location_checks.update({tuple(key): value for key, value in savedata["location_checks"]})
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in received_items.values()])} received items '
|
||||
f'for {len(received_items)} players')
|
||||
|
||||
async def send_msgs(websocket, msgs):
|
||||
if not websocket or not websocket.open or websocket.closed:
|
||||
|
@ -174,7 +199,9 @@ def register_location_checks(ctx : Context, team, slot, locations):
|
|||
if recvd_item.location == location and recvd_item.player == slot:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
ctx.location_checks[team, slot] += 1
|
||||
new_item = ReceivedItem(target_item, location, slot)
|
||||
recvd_items.append(new_item)
|
||||
if slot != target_player:
|
||||
|
@ -183,15 +210,32 @@ def register_location_checks(ctx : Context, team, slot, locations):
|
|||
found_items = True
|
||||
send_new_items(ctx)
|
||||
|
||||
if found_items and not ctx.disable_save:
|
||||
if found_items:
|
||||
save(ctx)
|
||||
|
||||
def save(ctx:Context):
|
||||
if not ctx.disable_save:
|
||||
try:
|
||||
with open(ctx.save_filename, "wb") as f:
|
||||
jsonstr = json.dumps((list(ctx.rom_names.items()),
|
||||
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
|
||||
jsonstr = json.dumps(ctx.get_save())
|
||||
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
def hint(ctx:Context, team, slot, item:str):
|
||||
found = 0
|
||||
seeked_item_id = Items.item_table[item][3]
|
||||
for check, result in ctx.locations.items():
|
||||
item_id, receiving_player = result
|
||||
if receiving_player == slot and item_id == seeked_item_id:
|
||||
location_id, finding_player = check
|
||||
hint = f"[Hint]: {ctx.player_names[(team, slot)]}'s {item} can be found at " \
|
||||
f"{get_location_name_from_address(location_id)} in {ctx.player_names[team, finding_player]}'s World"
|
||||
notify_team(ctx, team, hint)
|
||||
found += 1
|
||||
|
||||
return found
|
||||
|
||||
async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
||||
if type(cmd) is not str:
|
||||
await send_msgs(client.socket, [['InvalidCmd']])
|
||||
|
@ -277,15 +321,37 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||
|
||||
if args.startswith('!players'):
|
||||
notify_all(ctx, get_connected_players_string(ctx))
|
||||
if args.startswith('!forfeit'):
|
||||
elif args.startswith('!forfeit'):
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
if args.startswith('!countdown'):
|
||||
elif args.startswith('!countdown'):
|
||||
try:
|
||||
timer = int(args.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
timer = 10
|
||||
asyncio.create_task(countdown(ctx, timer))
|
||||
elif args.startswith("!hint"):
|
||||
points_available = ctx.location_check_points * ctx.location_checks[client.team, client.slot] - ctx.hint_cost*ctx.hints_used[client.team, client.slot]
|
||||
itemname = args[6:]
|
||||
if not itemname:
|
||||
notify_client(client, "Use !hint {itemname}. For example !hint Lamp. "
|
||||
f"A hint costs {ctx.hint_cost} points.\n"
|
||||
f"You have {points_available} points.")
|
||||
elif itemname in Items.item_table:
|
||||
if ctx.hint_cost: can_pay = points_available // ctx.hint_cost >= 1
|
||||
else: can_pay = True
|
||||
|
||||
if can_pay:
|
||||
found = hint(ctx, client.team, client.slot, itemname)
|
||||
ctx.hints_used[client.team, client.slot] += found
|
||||
if not found:
|
||||
notify_client(client, "No items found, points refunded.")
|
||||
else:
|
||||
save(ctx)
|
||||
else:
|
||||
notify_client(client, f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need {ctx.hint_cost}")
|
||||
else:
|
||||
notify_client(client, f'Item "{itemname}" not found.')
|
||||
def set_password(ctx : Context, password):
|
||||
ctx.password = password
|
||||
logging.warning('Password set to ' + password if password is not None else 'Password disabled')
|
||||
|
@ -341,19 +407,11 @@ async def console(ctx : Context):
|
|||
if command[0] == '/hint':
|
||||
for (team, slot), name in ctx.player_names.items():
|
||||
if len(command) == 1:
|
||||
print("Use /hint {Playername} {itemname}\nFor example /hint Berserker Lamp")
|
||||
logging.info("Use /hint {Playername} {itemname}\nFor example /hint Berserker Lamp")
|
||||
elif name.lower() == command[1].lower():
|
||||
item = " ".join(command[2:])
|
||||
if item in Items.item_table:
|
||||
seeked_item_id = Items.item_table[item][3]
|
||||
for check, result in ctx.locations.items():
|
||||
item_id, receiving_player = result
|
||||
if receiving_player == slot and item_id == seeked_item_id:
|
||||
location_id, finding_player = check
|
||||
name_finder = ctx.player_names[team, finding_player]
|
||||
hint = f"[Hint]: {name}'s {item} can be found at " \
|
||||
f"{get_location_name_from_address(location_id)} in {name_finder}'s World"
|
||||
notify_team(ctx, team, hint)
|
||||
hint(ctx, team, slot, item)
|
||||
else:
|
||||
logging.warning("Unknown item: " + item)
|
||||
if command[0][0] != '/':
|
||||
|
@ -371,11 +429,16 @@ async def main():
|
|||
parser.add_argument('--savefile', default=None)
|
||||
parser.add_argument('--disable_save', default=False, action='store_true')
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--location_check_points', default=1, type=int)
|
||||
parser.add_argument('--hint_cost', default=1000, type=int)
|
||||
args = parser.parse_args()
|
||||
|
||||
file_options = Utils.parse_yaml(open("host.yaml").read())["server_options"]
|
||||
for key, value in file_options.items():
|
||||
if value is not None:
|
||||
setattr(args, key, value)
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
|
||||
ctx = Context(args.host, args.port, args.password)
|
||||
ctx = Context(args.host, args.port, args.password, args.location_check_points, args.hint_cost)
|
||||
|
||||
ctx.data_filename = args.multidata
|
||||
|
||||
|
@ -418,16 +481,11 @@ async def main():
|
|||
try:
|
||||
with open(ctx.save_filename, 'rb') as f:
|
||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||
rom_names = jsonobj[0]
|
||||
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[1]}
|
||||
if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
|
||||
raise Exception('Save file mismatch, will start a new game')
|
||||
ctx.received_items = received_items
|
||||
logging.info('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
|
||||
ctx.set_save(jsonobj)
|
||||
except FileNotFoundError:
|
||||
logging.error('No save data found, starting a new game')
|
||||
except Exception as e:
|
||||
logging.info(e)
|
||||
logging.exception(e)
|
||||
|
||||
ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
|
||||
await ctx.server
|
||||
|
|
|
@ -10,18 +10,11 @@ import ModuleUpdate
|
|||
|
||||
ModuleUpdate.update()
|
||||
|
||||
from yaml import load
|
||||
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
from Utils import parse_yaml
|
||||
from Rom import get_sprite_from_name
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
|
||||
parse_yaml = functools.partial(load, Loader=Loader)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
8
Utils.py
8
Utils.py
|
@ -127,3 +127,11 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
|||
basemd5 = hashlib.md5()
|
||||
basemd5.update(new_rom_data)
|
||||
return "New Rom Hash: " + basemd5.hexdigest()
|
||||
|
||||
from yaml import load
|
||||
import functools
|
||||
|
||||
try: from yaml import CLoader as Loader
|
||||
except ImportError: from yaml import Loader
|
||||
|
||||
parse_yaml = functools.partial(load, Loader=Loader)
|
|
@ -0,0 +1,37 @@
|
|||
#options for MultiServer
|
||||
#null means nothing, for the server this means to default the value
|
||||
#these overwrite command line arguments!
|
||||
server_options:
|
||||
host: null
|
||||
port: null
|
||||
password: null
|
||||
multidata: null
|
||||
savefile: null
|
||||
disable_save: null
|
||||
loglevel: null
|
||||
#Client hint system
|
||||
#points given to player for each acquired item
|
||||
location_check_points: 1
|
||||
#point cost to receive a hint via !hint for players
|
||||
hint_cost: 1000
|
||||
#options for MultiMystery.py
|
||||
multi_mystery_options:
|
||||
#Where to place the resulting files
|
||||
output_path: "MultiMystery"
|
||||
#location of your Enemizer CLI, available here: https://github.com/Bonta0/Enemizer/releases
|
||||
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
#folder from which the player yaml files are pulled from
|
||||
player_files_path: "Players"
|
||||
#automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
|
||||
#does nothing if the name is not found
|
||||
#example: player_name = "Berserker"
|
||||
player_name: "" # the hosts name
|
||||
#Zip the resulting roms
|
||||
#0 -> Don't
|
||||
#1 -> Create a zip
|
||||
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
|
||||
zip_roms: 1
|
||||
#create a spoiler file
|
||||
create_spoiler: 1
|
||||
#create roms flagged as race roms
|
||||
race: 0
|
Loading…
Reference in New Issue