| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | from collections import deque, Counter | 
					
						
							|  |  |  | from contextlib import redirect_stdout | 
					
						
							|  |  |  | import functools | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  | import settings | 
					
						
							| 
									
										
										
										
											2022-12-09 17:54:29 -08:00
										 |  |  | import threading | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  | from typing import Any, ClassVar | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | import os | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  | from typing_extensions import override | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-19 15:29:13 -08:00
										 |  |  | from BaseClasses import LocationProgressType, MultiWorld, Item, CollectionState, Entrance, Tutorial | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | from .gen_data import GenData | 
					
						
							| 
									
										
										
										
											2024-09-18 12:09:47 -07:00
										 |  |  | from .logic import ZillionLogicCache | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | from .region import ZillionLocation, ZillionRegion | 
					
						
							| 
									
										
										
										
											2024-05-19 11:36:47 -07:00
										 |  |  | from .options import ZillionOptions, validate, z_option_groups | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  | from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     loc_name_to_id as _loc_name_to_id, make_id_to_others, \ | 
					
						
							|  |  |  |     zz_reg_name_to_reg_name, base_id | 
					
						
							| 
									
										
										
										
											2025-01-19 15:29:13 -08:00
										 |  |  | from .item import ZillionItem, get_classification | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  | from .patch import ZillionPatch | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | from zilliandomizer.system import System | 
					
						
							|  |  |  | from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem | 
					
						
							|  |  |  | from zilliandomizer.logic_components.locations import Location as ZzLocation, Req | 
					
						
							| 
									
										
										
										
											2025-01-19 15:31:09 -08:00
										 |  |  | from zilliandomizer.map_gen.region_maker import DEAD_END_SUFFIX | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | from zilliandomizer.options import Chars | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 07:23:02 -08:00
										 |  |  | from worlds.AutoWorld import World, WebWorld | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  | class ZillionSettings(settings.Group): | 
					
						
							|  |  |  |     class RomFile(settings.UserFilePath): | 
					
						
							|  |  |  |         """File name of the Zillion US rom""" | 
					
						
							|  |  |  |         description = "Zillion US ROM File" | 
					
						
							|  |  |  |         copy_to = "Zillion (UE) [!].sms" | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |         assert ZillionPatch.hash | 
					
						
							|  |  |  |         md5s = [ZillionPatch.hash] | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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 .sfc file with | 
					
						
							|  |  |  |         RetroArch doesn't make it easy to launch a game from the command line. | 
					
						
							|  |  |  |         You have to know the path to the emulator core library on the user's computer. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     rom_file: RomFile = RomFile(RomFile.copy_to) | 
					
						
							| 
									
										
										
										
											2024-11-29 12:17:56 -08:00
										 |  |  |     rom_start: RomStart | bool = RomStart("retroarch") | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | class ZillionWebWorld(WebWorld): | 
					
						
							|  |  |  |     theme = "stone" | 
					
						
							|  |  |  |     tutorials = [Tutorial( | 
					
						
							|  |  |  |         "Multiworld Setup Guide", | 
					
						
							|  |  |  |         "A guide to playing Zillion randomizer.", | 
					
						
							|  |  |  |         "English", | 
					
						
							|  |  |  |         "setup_en.md", | 
					
						
							|  |  |  |         "setup/en", | 
					
						
							| 
									
										
										
										
											2025-04-20 04:07:17 -07:00
										 |  |  |         ["beauxq"], | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     )] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 11:36:47 -07:00
										 |  |  |     option_groups = z_option_groups | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | class ZillionWorld(World): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Zillion is a metroidvania style game released in 1987 for the 8-bit Sega Master System. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion). | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     game = "Zillion" | 
					
						
							|  |  |  |     web = ZillionWebWorld() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |     options_dataclass = ZillionOptions | 
					
						
							| 
									
										
										
										
											2023-12-06 09:23:43 -08:00
										 |  |  |     options: ZillionOptions  # type: ignore | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     settings: ClassVar[ZillionSettings]  # type: ignore | 
					
						
							| 
									
										
										
										
											2023-12-06 09:23:43 -08:00
										 |  |  |     # these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486 | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-06 19:14:25 -08:00
										 |  |  |     topology_present = True  # indicate if world type has any meaningful layout/pathing | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # map names to their IDs | 
					
						
							| 
									
										
										
										
											2023-03-06 19:14:25 -08:00
										 |  |  |     item_name_to_id = _item_name_to_id | 
					
						
							|  |  |  |     location_name_to_id = _loc_name_to_id | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     logger: logging.Logger | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     class LogStreamInterface: | 
					
						
							|  |  |  |         logger: logging.Logger | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         buffer: list[str] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         def __init__(self, logger: logging.Logger) -> None: | 
					
						
							|  |  |  |             self.logger = logger | 
					
						
							|  |  |  |             self.buffer = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def write(self, msg: str) -> None: | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |             if msg.endswith("\n"): | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |                 self.buffer.append(msg[:-1]) | 
					
						
							|  |  |  |                 self.logger.debug("".join(self.buffer)) | 
					
						
							|  |  |  |                 self.buffer = [] | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.buffer.append(msg) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def flush(self) -> None: | 
					
						
							|  |  |  |             pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     lsi: LogStreamInterface | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     id_to_zz_item: dict[int, ZzItem] | None = None | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     zz_system: System | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     _item_counts: Counter[str] = Counter() | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     These are the items counts that will be in the game, | 
					
						
							|  |  |  |     which might be different from the item counts the player asked for in options | 
					
						
							|  |  |  |     (if the player asked for something invalid). | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     my_locations: list[ZillionLocation] = [] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ | 
					
						
							| 
									
										
										
										
											2025-01-19 23:20:45 +00:00
										 |  |  |     finalized_gen_data: GenData | None | 
					
						
							|  |  |  |     """ Finalized generation data needed by `generate_output` and by `fill_slot_data`. """ | 
					
						
							|  |  |  |     item_locations_finalization_lock: threading.Lock | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     This lock is used in `generate_output` and `fill_slot_data` to ensure synchronized access to `finalized_gen_data`, | 
					
						
							|  |  |  |     so that whichever is run first can finalize the item locations while the other waits. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     logic_cache: ZillionLogicCache | None = None | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     def __init__(self, world: MultiWorld, player: int) -> None: | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         super().__init__(world, player) | 
					
						
							|  |  |  |         self.logger = logging.getLogger("Zillion") | 
					
						
							|  |  |  |         self.lsi = ZillionWorld.LogStreamInterface(self.logger) | 
					
						
							|  |  |  |         self.zz_system = System() | 
					
						
							| 
									
										
										
										
											2025-01-19 23:20:45 +00:00
										 |  |  |         self.finalized_gen_data = None | 
					
						
							|  |  |  |         self.item_locations_finalization_lock = threading.Lock() | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _make_item_maps(self, start_char: Chars) -> None: | 
					
						
							|  |  |  |         _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) | 
					
						
							|  |  |  |         self.id_to_zz_item = id_to_zz_item | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def generate_early(self) -> None: | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |         zz_op, item_counts = validate(self.options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if zz_op.early_scope: | 
					
						
							|  |  |  |             self.multiworld.early_items[self.player]["Scope"] = 1 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self._item_counts = item_counts | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         with redirect_stdout(self.lsi):  # type: ignore | 
					
						
							| 
									
										
										
										
											2024-07-02 19:32:01 -07:00
										 |  |  |             self.zz_system.set_options(zz_op) | 
					
						
							|  |  |  |             self.zz_system.seed(self.random.randrange(1999999999)) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |             self.zz_system.make_map() | 
					
						
							| 
									
										
										
										
											2024-07-02 19:32:01 -07:00
										 |  |  |             self.zz_system.make_randomizer() | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # just in case the options changed anything (I don't think they do) | 
					
						
							|  |  |  |         assert self.zz_system.randomizer, "init failed" | 
					
						
							|  |  |  |         for zz_name in self.zz_system.randomizer.locations: | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |             if zz_name != "main": | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |                 assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \ | 
					
						
							|  |  |  |                     f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self._make_item_maps(zz_op.start_char) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def create_regions(self) -> None: | 
					
						
							|  |  |  |         assert self.zz_system.randomizer, "generate_early hasn't been called" | 
					
						
							|  |  |  |         assert self.id_to_zz_item, "generate_early hasn't been called" | 
					
						
							|  |  |  |         p = self.player | 
					
						
							| 
									
										
										
										
											2024-09-18 12:09:47 -07:00
										 |  |  |         logic_cache = ZillionLogicCache(p, self.zz_system.randomizer, self.id_to_zz_item) | 
					
						
							|  |  |  |         self.logic_cache = logic_cache | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |         w = self.multiworld | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         self.my_locations = [] | 
					
						
							| 
									
										
										
										
											2025-01-19 15:31:09 -08:00
										 |  |  |         dead_end_locations: list[ZillionLocation] = [] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self.zz_system.randomizer.place_canister_gun_reqs() | 
					
						
							| 
									
										
										
										
											2023-04-25 22:24:47 -07:00
										 |  |  |         # low probability that place_canister_gun_reqs() results in empty 1st sphere | 
					
						
							|  |  |  |         # testing code to force low probability event: | 
					
						
							|  |  |  |         # for zz_room_name in ["r01c2", "r02c0", "r02c7", "r03c5"]: | 
					
						
							|  |  |  |         #     for zz_loc in self.zz_system.randomizer.regions[zz_room_name].locations: | 
					
						
							|  |  |  |         #         zz_loc.req.gun = 2 | 
					
						
							|  |  |  |         if len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) == 0: | 
					
						
							|  |  |  |             self.logger.info("Zillion avoided rare empty 1st sphere.") | 
					
						
							|  |  |  |             for zz_loc in self.zz_system.randomizer.regions["r03c5"].locations: | 
					
						
							|  |  |  |                 zz_loc.req.gun = 1 | 
					
						
							|  |  |  |             assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         start = self.zz_system.randomizer.regions["start"] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         all_regions: dict[str, ZillionRegion] = {} | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         for here_zz_name, zz_r in self.zz_system.randomizer.regions.items(): | 
					
						
							|  |  |  |             here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name) | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |             all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w) | 
					
						
							|  |  |  |             self.multiworld.regions.append(all_regions[here_name]) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126) | 
					
						
							|  |  |  |         queue = deque([start]) | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         done: set[str] = set() | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         while len(queue): | 
					
						
							|  |  |  |             zz_here = queue.popleft() | 
					
						
							|  |  |  |             here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name) | 
					
						
							|  |  |  |             if here_name in done: | 
					
						
							|  |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |             here = all_regions[here_name] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |             for zz_loc in zz_here.locations: | 
					
						
							|  |  |  |                 # if local gun reqs didn't place "keyword" item | 
					
						
							|  |  |  |                 if not zz_loc.item: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     def access_rule_wrapped(zz_loc_local: ZzLocation, | 
					
						
							| 
									
										
										
										
											2024-09-18 12:09:47 -07:00
										 |  |  |                                             lc: ZillionLogicCache, | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |                                             cs: CollectionState) -> bool: | 
					
						
							| 
									
										
										
										
											2024-09-18 12:09:47 -07:00
										 |  |  |                         accessible = lc.cs_to_zz_locs(cs) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |                         return zz_loc_local in accessible | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 12:09:47 -07:00
										 |  |  |                     access_rule = functools.partial(access_rule_wrapped, zz_loc, logic_cache) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |                     loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name] | 
					
						
							|  |  |  |                     loc = ZillionLocation(zz_loc, self.player, loc_name, here) | 
					
						
							|  |  |  |                     loc.access_rule = access_rule | 
					
						
							|  |  |  |                     if not (limited_skill >= zz_loc.req): | 
					
						
							|  |  |  |                         loc.progress_type = LocationProgressType.EXCLUDED | 
					
						
							| 
									
										
										
										
											2024-01-14 06:48:30 -08:00
										 |  |  |                         self.options.exclude_locations.value.add(loc.name) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |                     here.locations.append(loc) | 
					
						
							|  |  |  |                     self.my_locations.append(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-19 15:31:09 -08:00
										 |  |  |                     if (( | 
					
						
							|  |  |  |                         zz_here.name.endswith(DEAD_END_SUFFIX) | 
					
						
							|  |  |  |                     ) or ( | 
					
						
							|  |  |  |                         (self.options.map_gen.value != self.options.map_gen.option_full) and | 
					
						
							|  |  |  |                         (loc.name in self.options.priority_dead_ends.vanilla_dead_ends) | 
					
						
							|  |  |  |                     ) or ( | 
					
						
							|  |  |  |                         loc.name in self.options.priority_dead_ends.always_dead_ends | 
					
						
							|  |  |  |                     )): | 
					
						
							|  |  |  |                         dead_end_locations.append(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |             for zz_dest in zz_here.connections.keys(): | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |                 dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name) | 
					
						
							|  |  |  |                 dest = all_regions[dest_name] | 
					
						
							|  |  |  |                 exit_ = Entrance(p, f"{here_name} to {dest_name}", here) | 
					
						
							|  |  |  |                 here.exits.append(exit_) | 
					
						
							|  |  |  |                 exit_.connect(dest) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 queue.append(zz_dest) | 
					
						
							|  |  |  |             done.add(here.name) | 
					
						
							| 
									
										
										
										
											2025-01-19 15:31:09 -08:00
										 |  |  |         if self.options.priority_dead_ends.value: | 
					
						
							|  |  |  |             self.options.priority_locations.value |= {loc.name for loc in dead_end_locations} | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def create_items(self) -> None: | 
					
						
							|  |  |  |         if not self.id_to_zz_item: | 
					
						
							|  |  |  |             self._make_item_maps("JJ") | 
					
						
							|  |  |  |             self.logger.warning("warning: called `create_items` without calling `generate_early` first") | 
					
						
							|  |  |  |         assert self.id_to_zz_item, "failed to get item maps" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # in zilliandomizer, the Randomizer class puts empties in the item pool to fill space, | 
					
						
							|  |  |  |         # but here in AP, empties are in the options from options.validate | 
					
						
							|  |  |  |         item_counts = self._item_counts | 
					
						
							|  |  |  |         self.logger.debug(item_counts) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for item_name, item_id in self.item_name_to_id.items(): | 
					
						
							|  |  |  |             zz_item = self.id_to_zz_item[item_id] | 
					
						
							|  |  |  |             if item_id >= (4 + base_id):  # normal item | 
					
						
							|  |  |  |                 if item_name in item_counts: | 
					
						
							|  |  |  |                     count = item_counts[item_name] | 
					
						
							|  |  |  |                     self.logger.debug(f"Zillion Items: {item_name}  {count}") | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |                     self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |             elif item_id < (3 + base_id) and zz_item.code == RESCUE: | 
					
						
							|  |  |  |                 # One of the 3 rescues will not be in the pool and its zz_item will be 'empty'. | 
					
						
							|  |  |  |                 self.logger.debug(f"Zillion Items: {item_name}  1") | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |                 self.multiworld.itempool.append(self.create_item(item_name)) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def generate_basic(self) -> None: | 
					
						
							|  |  |  |         assert self.zz_system.randomizer, "generate_early hasn't been called" | 
					
						
							|  |  |  |         # main location name is an alias | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name] | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |         self.multiworld.get_location(main_loc_name, self.player)\ | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |             .place_locked_item(self.create_item("Win")) | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |         self.multiworld.completion_condition[self.player] = \ | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |             lambda state: state.has("Win", self.player) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  |     @staticmethod | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:  # noqa: ANN401 | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  |         # item link pools are about to be created in main | 
					
						
							|  |  |  |         # JJ can't be an item link unless all the players share the same start_char | 
					
						
							|  |  |  |         # (The reason for this is that the JJ ZillionItem will have a different ZzItem depending | 
					
						
							|  |  |  |         #  on whether the start char is Apple or Champ, and the logic depends on that ZzItem.) | 
					
						
							|  |  |  |         for group in multiworld.groups.values(): | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |             if group["game"] == "Zillion" and "item_pool" in group: | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  |                 item_pool = group["item_pool"] | 
					
						
							| 
									
										
										
										
											2024-01-14 06:48:30 -08:00
										 |  |  |                 to_stay: Chars = "JJ" | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  |                 if "JJ" in item_pool: | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |                     group["players"] = group_players = set(group["players"]) | 
					
						
							|  |  |  |                     players_start_chars: list[tuple[int, Chars]] = [] | 
					
						
							| 
									
										
										
										
											2024-01-14 06:48:30 -08:00
										 |  |  |                     for player in group_players: | 
					
						
							|  |  |  |                         z_world = multiworld.worlds[player] | 
					
						
							|  |  |  |                         assert isinstance(z_world, ZillionWorld) | 
					
						
							|  |  |  |                         players_start_chars.append((player, z_world.options.start_char.get_char())) | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  |                     start_char_counts = Counter(sc for _, sc in players_start_chars) | 
					
						
							|  |  |  |                     # majority rules | 
					
						
							|  |  |  |                     if start_char_counts["Apple"] > start_char_counts["Champ"]: | 
					
						
							|  |  |  |                         to_stay = "Apple" | 
					
						
							|  |  |  |                     elif start_char_counts["Champ"] > start_char_counts["Apple"]: | 
					
						
							|  |  |  |                         to_stay = "Champ" | 
					
						
							|  |  |  |                     else:  # equal | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |                         choices: tuple[Chars, ...] = ("Apple", "Champ") | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |                         to_stay = multiworld.random.choice(choices) | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |                     for p, sc in players_start_chars: | 
					
						
							|  |  |  |                         if sc != to_stay: | 
					
						
							|  |  |  |                             group_players.remove(p) | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |                 group_world = group["world"] | 
					
						
							|  |  |  |                 assert isinstance(group_world, ZillionWorld) | 
					
						
							|  |  |  |                 group_world._make_item_maps(to_stay) | 
					
						
							| 
									
										
										
										
											2022-10-28 12:56:50 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def post_fill(self) -> None: | 
					
						
							|  |  |  |         """Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
 | 
					
						
							|  |  |  |         This happens before progression balancing,  so the items may not be in their final locations yet."""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.zz_system.post_fill() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-19 23:20:45 +00:00
										 |  |  |     def finalize_item_locations_thread_safe(self) -> GenData: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Call self.finalize_item_locations() and cache the result in a thread-safe manner so that either | 
					
						
							|  |  |  |         `generate_output` or `fill_slot_data` can finalize item locations without concern for which of the two functions | 
					
						
							|  |  |  |         is called first. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         # The lock is acquired when entering the context manager and released when exiting the context manager. | 
					
						
							|  |  |  |         with self.item_locations_finalization_lock: | 
					
						
							|  |  |  |             # If generation data has yet to be finalized, finalize it. | 
					
						
							|  |  |  |             if self.finalized_gen_data is None: | 
					
						
							|  |  |  |                 self.finalized_gen_data = self.finalize_item_locations() | 
					
						
							|  |  |  |         return self.finalized_gen_data | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |     def finalize_item_locations(self) -> GenData: | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         sync zilliandomizer item locations with AP item locations | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         return the data needed to generate output | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         assert self.zz_system.randomizer, "generate_early hasn't been called" | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         # debug_zz_loc_ids: dict[str, int] = {} | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         empty = zz_items[4] | 
					
						
							|  |  |  |         multi_item = empty  # a different patcher method differentiates empty from ap multi item | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         multi_items: dict[str, tuple[str, str]] = {}  # zz_loc_name to (item_name, player_name) | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |         for z_loc in self.multiworld.get_locations(self.player): | 
					
						
							|  |  |  |             assert isinstance(z_loc, ZillionLocation) | 
					
						
							| 
									
										
										
										
											2023-10-29 19:47:37 +01:00
										 |  |  |             # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) | 
					
						
							|  |  |  |             if z_loc.item is None: | 
					
						
							| 
									
										
										
										
											2024-05-13 11:31:15 -07:00
										 |  |  |                 self.logger.warning("generate_output location has no item - is that ok?") | 
					
						
							| 
									
										
										
										
											2023-10-29 19:47:37 +01:00
										 |  |  |                 z_loc.zz_loc.item = empty | 
					
						
							|  |  |  |             elif z_loc.item.player == self.player: | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |                 z_item = z_loc.item | 
					
						
							|  |  |  |                 assert isinstance(z_item, ZillionItem) | 
					
						
							| 
									
										
										
										
											2023-10-29 19:47:37 +01:00
										 |  |  |                 z_loc.zz_loc.item = z_item.zz_item | 
					
						
							|  |  |  |             else:  # another player's item | 
					
						
							|  |  |  |                 # print(f"put multi item in {z_loc.zz_loc.name}") | 
					
						
							|  |  |  |                 z_loc.zz_loc.item = multi_item | 
					
						
							|  |  |  |                 multi_items[z_loc.zz_loc.name] = ( | 
					
						
							|  |  |  |                     z_loc.item.name, | 
					
						
							| 
									
										
										
										
											2025-04-20 04:07:17 -07:00
										 |  |  |                     self.multiworld.get_player_name(z_loc.item.player), | 
					
						
							| 
									
										
										
										
											2023-10-29 19:47:37 +01:00
										 |  |  |                 ) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         # debug_zz_loc_ids.sort() | 
					
						
							|  |  |  |         # for name, id_ in debug_zz_loc_ids.items(): | 
					
						
							|  |  |  |         #     print(id_) | 
					
						
							|  |  |  |         # print("size:", len(debug_zz_loc_ids)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         # debug_loc_to_id: dict[str, int] = {} | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         # regions = self.zz_randomizer.regions | 
					
						
							|  |  |  |         # for region in regions.values(): | 
					
						
							|  |  |  |         #     for loc in region.locations: | 
					
						
							|  |  |  |         #         if loc.name not in self.zz_randomizer.locations: | 
					
						
							|  |  |  |         #             print(f"region {region.name} had location {loc.name} not in locations") | 
					
						
							|  |  |  |         #         debug_loc_to_id[loc.name] = id(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # verify that every location got an item | 
					
						
							|  |  |  |         for zz_loc in self.zz_system.randomizer.locations.values(): | 
					
						
							|  |  |  |             assert zz_loc.item, ( | 
					
						
							|  |  |  |                 f"location {self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name]} " | 
					
						
							|  |  |  |                 f"in world {self.player} didn't get an item" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |         game_id = self.multiworld.player_name[self.player].encode() + b"\x00" + self.multiworld.seed_name[-6:].encode() | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |         return GenData(multi_items, self.zz_system.get_game(), game_id) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |     def generate_output(self, output_directory: str) -> None: | 
					
						
							|  |  |  |         """This method gets called from a threadpool, do not use multiworld.random here.
 | 
					
						
							|  |  |  |         If you need any last-second randomization, use self.random instead."""
 | 
					
						
							| 
									
										
										
										
											2025-01-19 23:20:45 +00:00
										 |  |  |         gen_data = self.finalize_item_locations_thread_safe() | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |         out_file_base = self.multiworld.get_out_file_name_base(self.player) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |         patch_file_name = os.path.join(output_directory, f"{out_file_base}{ZillionPatch.patch_file_ending}") | 
					
						
							|  |  |  |         patch = ZillionPatch(patch_file_name, | 
					
						
							|  |  |  |                              player=self.player, | 
					
						
							|  |  |  |                              player_name=self.multiworld.player_name[self.player], | 
					
						
							|  |  |  |                              gen_data_str=gen_data.to_json()) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         patch.write() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |         self.logger.debug(f"Zillion player {self.player} finished generate_output") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |     def fill_slot_data(self) -> ZillionSlotInfo:  # json of WebHostLib.models.Slot | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         """Fill in the `slot_data` field in the `Connected` network package.
 | 
					
						
							|  |  |  |         This is a way the generator can give custom data to the client. | 
					
						
							|  |  |  |         The client will receive this as JSON in the `Connected` response."""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # TODO: share a TypedDict data structure with client | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # TODO: tell client which canisters are keywords | 
					
						
							|  |  |  |         # so it can open and get those when restoring doors | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-19 23:20:45 +00:00
										 |  |  |         game = self.finalize_item_locations_thread_safe().zz_game | 
					
						
							| 
									
										
										
										
											2024-03-03 13:10:14 -08:00
										 |  |  |         return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # end of ordered Main.py calls | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def create_item(self, name: str) -> Item: | 
					
						
							|  |  |  |         """Create an item for this world type and player.
 | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |         Warning: this may be called with self.multiworld = None, for example by MultiServer"""
 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |         item_id = _item_name_to_id[name] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not self.id_to_zz_item: | 
					
						
							|  |  |  |             self._make_item_maps("JJ") | 
					
						
							|  |  |  |             self.logger.warning("warning: called `create_item` without calling `generate_early` first") | 
					
						
							|  |  |  |         assert self.id_to_zz_item, "failed to get item maps" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         zz_item = self.id_to_zz_item[item_id] | 
					
						
							| 
									
										
										
										
											2025-01-19 15:29:13 -08:00
										 |  |  |         classification = get_classification(name, zz_item, self._item_counts) | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         z_item = ZillionItem(name, classification, item_id, self.player, zz_item) | 
					
						
							|  |  |  |         return z_item | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 12:25:01 -08:00
										 |  |  |     @override | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     def get_filler_item_name(self) -> str: | 
					
						
							|  |  |  |         """Called when the item pool needs to be filled with additional items to match location count.""" | 
					
						
							|  |  |  |         return "Empty" |