Skip to content

Commit

Permalink
Add executable apmp1 file, move output iso generation to client
Browse files Browse the repository at this point in the history
  • Loading branch information
hesto2 committed May 20, 2024
1 parent 53982c2 commit dfa9f1f
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 45 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/build/target
__pycache__
*.apmp1
*.iso
18 changes: 18 additions & 0 deletions Container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os
import zipfile
from worlds.Files import APContainer


class MetroidPrimeContainer(APContainer):
game: str = 'Metroid Prime'

def __init__(self, config_json: str, outfile_name: str, output_directory: str,
player=None, player_name: str = "", server: str = ""):
self.config_json = config_json
self.config_path = "config.json"
container_path = os.path.join(output_directory, outfile_name + ".apmp1")
super().__init__(container_path, player, player_name, server)

def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
opened_zipfile.writestr(self.config_path, self.config_json)
super().write_contents(opened_zipfile)
File renamed without changes.
11 changes: 11 additions & 0 deletions Metroid Prime.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Metroid Prime:
# TODO: Add examples here
progression_balancing: 50
accessibility: items
spring_ball: 'false'
required_artifacts: 12
final_bosses: both
death_link: 'true'
description: 'Generated by https://archipelago.gg.'
game: Metroid Prime
name: Player_1
89 changes: 69 additions & 20 deletions MetroidPrimeClient.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import asyncio
import logging
import json
import multiprocessing
import os
import subprocess
import sys
import traceback
import zipfile
import py_randomprime

from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, logger, server_loop, gui_enabled
from NetUtils import ClientStatus, NetworkItem
Expand Down Expand Up @@ -212,20 +218,67 @@ async def _handle_game_not_ready(ctx: MetroidPrimeContext):
await asyncio.sleep(3)


def main(connect=None, password=None, name=None):
Utils.init_logging("Metroid Prime Client")

async def _main(connect, password, name):
ctx = MetroidPrimeContext(connect, password)
ctx.auth = name
ctx.server_task = asyncio.create_task(
server_loop(ctx), name="ServerLoop")
async def run_game(romfile):
auto_start = Utils.get_options()["metroidprime_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


async def patch_and_run_game(apmp1_file: str):
apmp1_file = os.path.abspath(apmp1_file)
input_iso_path = Utils.get_options()["metroidprime_options"]["rom_file"]
base_name = os.path.splitext(apmp1_file)[0]
output_path = base_name + '.iso'

config_json_file = None
if zipfile.is_zipfile(apmp1_file):
for name in zipfile.ZipFile(apmp1_file).namelist():
if name == 'config.json':
config_json_file = name
break

config_json = None
with zipfile.ZipFile(apmp1_file) as zip_file:
with zip_file.open(config_json_file) as file:
config_json = file.read().decode("utf-8")
config_json = json.loads(config_json)

notifier = py_randomprime.ProgressNotifier(
lambda progress, message: print("Generating ISO: ", progress, message))
py_randomprime.patch_iso(input_iso_path, output_path, config_json, notifier)


def launch():
Utils.init_logging("MetroidPrime Client")

async def main():
multiprocessing.freeze_support()
logger.info("main")
parser = get_base_parser()
parser.add_argument('apmp1_file', default="", type=str, nargs="?",
help='Path to an apmp1 file')
raw_argstring = ' '.join(sys.argv[1:])
logger.info(raw_argstring)
args = parser.parse_args()


if args.apmp1_file:
logger.info("APMP1 file supplied, beginning patching process...")
Utils.async_start(patch_and_run_game(args.apmp1_file))

ctx = MetroidPrimeContext(args.connect, args.password)
logger.info("Connecting to server...")
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
if gui_enabled:
ctx.run_gui()
await asyncio.sleep(1)
ctx.run_cli()

ctx.dolphin_sync_task = asyncio.create_task(
dolphin_sync_task(ctx), name="DolphinSync")
logger.info("Running game...")
ctx.dolphin_sync_task = asyncio.create_task(dolphin_sync_task(ctx), name="Dolphin Sync")

await ctx.exit_event.wait()
ctx.server_address = None
Expand All @@ -239,14 +292,10 @@ async def _main(connect, password, name):
import colorama

colorama.init()
asyncio.run(_main(connect, password, name))

asyncio.run(main())
colorama.deinit()


if __name__ == "__main__":
parser = get_base_parser()
parser.add_argument('--name', default=None,
help="Slot Name to connect as.")
args = parser.parse_args()
logger.setLevel(logging.DEBUG)
main(args.connect, args.password, args.name)
if __name__ == '__main__':
launch()
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The goal of randomized Metroid Prime depends on your selected victory condition.
## Which items can be in another player's world?

All suit upgrades except the following can be found in another player's world:

- Power Suit
- Power Beam
- Combat Visor
Expand All @@ -27,9 +28,11 @@ Multiworld items appear as a Metroid Trophy in the game.
## When the player receives an item, what happens?

The player will immediately have their suit inventory updated and receive a notification in the Client.
* __Currently there is no in-game HUD notification for this, although this is actively being worked on__

- **Currently there is no in-game HUD notification for this, although this is actively being worked on**

## FAQs

- What happens if I pickup an item without having the client running?

- In order for Metroid Prime Archipelago to function correctly, the Client should always be running whenever you are playing through your game. Due to the way location checks are handled, the client will not be aware of any item you have picked up when it is not running except the one you most recently picked up.
Expand All @@ -38,3 +41,5 @@ The player will immediately have their suit inventory updated and receive a noti

- It hasn't been tested extensively, but so far it appears yes

- Does this work with version x, y, or z?
- Currently we only support version `0-00`
39 changes: 21 additions & 18 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pathlib import Path
from typing import Any, Dict, List
import typing
from BaseClasses import Item, Tutorial, ItemClassification
from worlds.metroidprime.Container import MetroidPrimeContainer
from .Items import MetroidPrimeItem, suit_upgrade_table, artifact_table, item_table, custom_suit_upgrade_table
from .PrimeOptions import MetroidPrimeOptions
from .Locations import every_location
Expand All @@ -11,27 +13,36 @@
from ..AutoWorld import WebWorld
import py_randomprime
import settings
from worlds.LauncherComponents import Component, Type, components, launch_subprocess
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess


def run_client():
def run_client(_):
print("Running Metroid Prime Client")
from MetroidPrimeClient import main # lazy import
launch_subprocess(main, name="MetroidPrimeClient")
from .MetroidPrimeClient import launch
launch_subprocess(launch, name="MetroidPrimeClient")


components.append(
Component("Metroid Prime Client", func=run_client, component_type=Type.CLIENT)
Component("Metroid Prime Client", func=run_client, component_type=Type.CLIENT,
file_identifier=SuffixIdentifier(".apmp1"))
)


class MetroidPrimeSettings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the Metroid Prime ISO"""
description = "Metroid Prime (US) v1.0 ISO file"
copy_to = "prime.iso"
copy_to = "Metroid_Prime.iso"

rom_file: RomFile = RomFile.copy_to
class RomStart(str):
"""
Set this to false to never autostart a rom (such as after patching),
true for operating system default program
Alternatively, a path to a program to open the .iso file with
"""

rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: typing.Union[RomStart, bool] = True


class MetroidPrimeWeb(WebWorld):
Expand Down Expand Up @@ -128,19 +139,11 @@ def generate_output(self, output_directory: str) -> None:
configjson = make_config(self)
# convert configjson to json
import json

configjsons = json.dumps(configjson, indent=4)
# TODO: Remove this later
# write configjson to a file for review
with open("config.json", "w") as file:
file.write(configjsons)
notifier = py_randomprime.ProgressNotifier(
lambda progress, message: print("Generating ISO: ", progress, message))

input_iso_path = Path(settings.get_settings().metroidprime_options.rom_file)
output_iso_path = Path(f"{output_directory}\prime_out.iso")

py_randomprime.patch_iso(input_iso_path, output_iso_path, configjson, notifier)
outfile_name = self.multiworld.get_out_file_name_base(self.player)
apmp1 = MetroidPrimeContainer(configjsons, outfile_name, output_directory, player=self.player, player_name=self.multiworld.get_player_name(self.player))
apmp1.write()

def fill_slot_data(self) -> Dict[str, Any]:

Expand Down
8 changes: 8 additions & 0 deletions build/apworld.ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git*
__pycache__
build
LICENSE.md
README.md
requirements.txt
Metroid Prime.yaml
*.iso
Loading

0 comments on commit dfa9f1f

Please sign in to comment.