Skip to content

Commit

Permalink
Zork Grand Inquisitor: Implement New Game (ArchipelagoMW#2539)
Browse files Browse the repository at this point in the history
Adds Archipelago support for Zork Grand Inquisitor, the 1997 point-and-click PC adventure game.

The client (based on `CommonClient`), on top of its regular Archipelago duties, fully handles the randomization of the game and the monitoring / modification of the game state. No game modding needed at all; the player is ready to play an Archipelago seed if they can play the vanilla game through ScummVM.

The "reverse engineering" (there's likely a better term for this...) of the game is my own original work and I included an MIT license at the root of my world directory.

A PopTracker pack was also created to help people learn the game: https://github.com/SerpentAI/ZorkGrandInquisitorAPTracker
  • Loading branch information
nbrochu authored and EmilyV99 committed Apr 15, 2024
1 parent 5053f72 commit dc3fb75
Show file tree
Hide file tree
Showing 24 changed files with 9,239 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Currently, the following games are supported:
* TUNIC
* Kirby's Dream Land 3
* Celeste 64
* Zork Grand Inquisitor

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
Expand Down
3 changes: 3 additions & 0 deletions docs/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@
# Zillion
/worlds/zillion/ @beauxq

# Zork Grand Inquisitor
/worlds/zork_grand_inquisitor/ @nbrochu

##################################
## Disabled Unmaintained Worlds ##
##################################
Expand Down
21 changes: 21 additions & 0 deletions worlds/zork_grand_inquisitor/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Serpent.AI

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
17 changes: 17 additions & 0 deletions worlds/zork_grand_inquisitor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import worlds.LauncherComponents as LauncherComponents

from .world import ZorkGrandInquisitorWorld


def launch_client() -> None:
from .client import main
LauncherComponents.launch_subprocess(main, name="ZorkGrandInquisitorClient")


LauncherComponents.components.append(
LauncherComponents.Component(
"Zork Grand Inquisitor Client",
func=launch_client,
component_type=LauncherComponents.Type.CLIENT
)
)
188 changes: 188 additions & 0 deletions worlds/zork_grand_inquisitor/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import asyncio

import CommonClient
import NetUtils
import Utils

from typing import Any, Dict, List, Optional, Set, Tuple

from .data_funcs import item_names_to_id, location_names_to_id, id_to_items, id_to_locations, id_to_goals
from .enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations
from .game_controller import GameController


class ZorkGrandInquisitorCommandProcessor(CommonClient.ClientCommandProcessor):
def _cmd_zork(self) -> None:
"""Attach to an open Zork Grand Inquisitor process."""
result: bool = self.ctx.game_controller.open_process_handle()

if result:
self.ctx.process_attached_at_least_once = True
self.output("Successfully attached to Zork Grand Inquisitor process.")
else:
self.output("Failed to attach to Zork Grand Inquisitor process.")

def _cmd_brog(self) -> None:
"""List received Brog items."""
self.ctx.game_controller.list_received_brog_items()

def _cmd_griff(self) -> None:
"""List received Griff items."""
self.ctx.game_controller.list_received_griff_items()

def _cmd_lucy(self) -> None:
"""List received Lucy items."""
self.ctx.game_controller.list_received_lucy_items()

def _cmd_hotspots(self) -> None:
"""List received Hotspots."""
self.ctx.game_controller.list_received_hotspots()


class ZorkGrandInquisitorContext(CommonClient.CommonContext):
tags: Set[str] = {"AP"}
game: str = "Zork Grand Inquisitor"
command_processor: CommonClient.ClientCommandProcessor = ZorkGrandInquisitorCommandProcessor
items_handling: int = 0b111
want_slot_data: bool = True

item_name_to_id: Dict[str, int] = item_names_to_id()
location_name_to_id: Dict[str, int] = location_names_to_id()

id_to_items: Dict[int, ZorkGrandInquisitorItems] = id_to_items()
id_to_locations: Dict[int, ZorkGrandInquisitorLocations] = id_to_locations()

game_controller: GameController

controller_task: Optional[asyncio.Task]

process_attached_at_least_once: bool
can_display_process_message: bool

def __init__(self, server_address: Optional[str], password: Optional[str]) -> None:
super().__init__(server_address, password)

self.game_controller = GameController(logger=CommonClient.logger)

self.controller_task = None

self.process_attached_at_least_once = False
self.can_display_process_message = True

def run_gui(self) -> None:
from kvui import GameManager

class TextManager(GameManager):
logging_pairs: List[Tuple[str, str]] = [("Client", "Archipelago")]
base_title: str = "Archipelago Zork Grand Inquisitor Client"

self.ui = TextManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")

async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super().server_auth(password_requested)

await self.get_username()
await self.send_connect()

def on_package(self, cmd: str, _args: Any) -> None:
if cmd == "Connected":
self.game = self.slot_info[self.slot].game

# Options
self.game_controller.option_goal = id_to_goals()[_args["slot_data"]["goal"]]
self.game_controller.option_deathsanity = _args["slot_data"]["deathsanity"] == 1

self.game_controller.option_grant_missable_location_checks = (
_args["slot_data"]["grant_missable_location_checks"] == 1
)

async def controller(self):
while not self.exit_event.is_set():
await asyncio.sleep(0.1)

# Enqueue Received Item Delta
network_item: NetUtils.NetworkItem
for network_item in self.items_received:
item: ZorkGrandInquisitorItems = self.id_to_items[network_item.item]

if item not in self.game_controller.received_items:
if item not in self.game_controller.received_items_queue:
self.game_controller.received_items_queue.append(item)

# Game Controller Update
if self.game_controller.is_process_running():
self.game_controller.update()
self.can_display_process_message = True
else:
process_message: str

if self.process_attached_at_least_once:
process_message = (
"Lost connection to Zork Grand Inquisitor process. Please restart the game and use the /zork "
"command to reattach."
)
else:
process_message = (
"Please use the /zork command to attach to a running Zork Grand Inquisitor process."
)

if self.can_display_process_message:
CommonClient.logger.info(process_message)
self.can_display_process_message = False

# Send Checked Locations
checked_location_ids: List[int] = list()

while len(self.game_controller.completed_locations_queue) > 0:
location: ZorkGrandInquisitorLocations = self.game_controller.completed_locations_queue.popleft()
location_id: int = self.location_name_to_id[location.value]

checked_location_ids.append(location_id)

await self.send_msgs([
{
"cmd": "LocationChecks",
"locations": checked_location_ids
}
])

# Check for Goal Completion
if self.game_controller.goal_completed:
await self.send_msgs([
{
"cmd": "StatusUpdate",
"status": CommonClient.ClientStatus.CLIENT_GOAL
}
])


def main() -> None:
Utils.init_logging("ZorkGrandInquisitorClient", exception_logger="Client")

async def _main():
ctx: ZorkGrandInquisitorContext = ZorkGrandInquisitorContext(None, None)

ctx.server_task = asyncio.create_task(CommonClient.server_loop(ctx), name="server loop")
ctx.controller_task = asyncio.create_task(ctx.controller(), name="ZorkGrandInquisitorController")

if CommonClient.gui_enabled:
ctx.run_gui()

ctx.run_cli()

await ctx.exit_event.wait()
await ctx.shutdown()

import colorama

colorama.init()

asyncio.run(_main())

colorama.deinit()


if __name__ == "__main__":
main()
Empty file.
Loading

0 comments on commit dc3fb75

Please sign in to comment.