1212import pkgutil
1313import shutil
1414import sys
15+ import tempfile
16+
1517
1618from worlds .dk64 .ap_version import version as ap_version
1719
2426except ImportError :
2527 pass
2628if baseclasses_loaded :
27- baseclasses_path = os .path .dirname (os .path .dirname (BaseClasses .__file__ ))
28- if not baseclasses_path .endswith ("lib" ):
29- baseclasses_path = os .path .join (baseclasses_path , "lib" )
3029
3130 def display_error_box (title : str , text : str ) -> bool | None :
3231 """Display an error message box."""
@@ -38,32 +37,26 @@ def display_error_box(title: str, text: str) -> bool | None:
3837 root .update ()
3938
4039 def copy_dependencies (zip_path , file ):
41- """Copy a ZIP file from the package to a local directory, extracts its contents.
40+ """Copy a ZIP file from the package to a temporary directory, extracts its contents.
4241
43- Ensures the destination directory exists.
42+ Ensures the temporary directory exists.
4443 Args:
4544 zip_path (str): The relative path to the ZIP file within the package.
4645 Behavior:
47- - Creates a `./lib` directory if it does not exist.
46+ - Creates a temporary directory if it does not exist.
4847 - Reads the ZIP file from the package using `pkgutil.get_data`.
49- - Writes the ZIP file to the `./lib` directory if it does not already exist.
50- - Extracts the contents of the ZIP file into the `./lib` directory.
48+ - Writes the ZIP file to the temporary directory if it does not already exist.
49+ - Extracts the contents of the ZIP file into the temporary directory.
5150 Prints:
5251 - A message if the ZIP file could not be read.
5352 - A message when the ZIP file is successfully copied.
5453 - A message when the ZIP file is successfully extracted.
5554 """
56- # Find the path of BaseClasses, we want to work in the AP directory
57- # This is a bit of a hack, but it works
58- # Get the path of BaseClasses
59- dest_dir = baseclasses_path
60- # if baseclasses_path does not end in lib, add lib to the end
55+ # Create a temporary directory
56+ temp_dir = tempfile .mkdtemp ()
6157
62- zip_dest = os .path .join (dest_dir , file )
58+ zip_dest = os .path .join (temp_dir , file )
6359 try :
64- # Ensure the destination directory exists
65- os .makedirs (dest_dir , exist_ok = True )
66-
6760 # Load the ZIP file from the package
6861 zip_data = pkgutil .get_data (__name__ , zip_path )
6962 # Check if the zip already exists in the destination
@@ -78,26 +71,37 @@ def copy_dependencies(zip_path, file):
7871
7972 # Extract the ZIP file
8073 with zipfile .ZipFile (zip_dest , "r" ) as zip_ref :
81- zip_ref .extractall (dest_dir )
82- print (f"Extracted { zip_dest } into { dest_dir } " )
74+ zip_ref .extractall (temp_dir )
75+ print (f"Extracted { zip_dest } into { temp_dir } " )
76+
8377 except PermissionError :
8478 display_error_box ("Permission Error" , "Unable to install Dependencies to AP, please try to install AP as an admin." )
8579 raise PermissionError ("Permission Error: Unable to install Dependencies to AP, please try to install AP as an admin." )
8680
81+ # Add the temporary directory to sys.path
82+ if temp_dir not in sys .path :
83+ sys .path .insert (0 , temp_dir )
84+
8785 platform_type = sys .platform
88- # if the file pyxdelta.cp310-win_amd64.pyd exists, delete pyxdelta.cp310-win_amd64.pyd and PIL and pillow-10.3.0.dist-info and pyxdelta-0.2.0.dist-info
89- if os .path .exists (f"{ baseclasses_path } /pyxdelta.cp310-win_amd64.pyd" ):
90- os .remove (f"{ baseclasses_path } /pyxdelta.cp310-win_amd64.pyd" )
91- if os .path .exists (f"{ baseclasses_path } /PIL" ):
92- shutil .rmtree (f"{ baseclasses_path } /PIL" )
93- if os .path .exists (f"{ baseclasses_path } /pillow-10.3.0.dist-info" ):
94- shutil .rmtree (f"{ baseclasses_path } /pillow-10.3.0.dist-info" )
95- if os .path .exists (f"{ baseclasses_path } /pyxdelta-0.2.0.dist-info" ):
96- shutil .rmtree (f"{ baseclasses_path } /pyxdelta-0.2.0.dist-info" )
97- if os .path .exists (f"{ baseclasses_path } /windows.zip" ):
98- os .remove (f"{ baseclasses_path } /windows.zip" )
99- if os .path .exists (f"{ baseclasses_path } /linux.zip" ):
100- os .remove (f"{ baseclasses_path } /linux.zip" )
86+ baseclasses_path = os .path .dirname (os .path .dirname (BaseClasses .__file__ ))
87+ if not baseclasses_path .endswith ("lib" ):
88+ baseclasses_path = os .path .join (baseclasses_path , "lib" )
89+ # Remove ANY PIL folders from the baseclasses_path
90+ # Or Pyxdelta or pillow folders
91+ try :
92+ for folder in os .listdir (baseclasses_path ):
93+ if folder .startswith ("PIL" ) or folder .startswith ("pyxdelta" ) or folder .startswith ("pillow" ):
94+ folder_path = os .path .join (baseclasses_path , folder )
95+ if os .path .isdir (folder_path ):
96+ shutil .rmtree (folder_path )
97+ elif os .path .isfile (folder_path ):
98+ os .remove (folder_path )
99+ # Also if its windows.zip or linux.zip, remove it
100+ if folder .startswith ("windows.zip" ) or folder .startswith ("linux.zip" ):
101+ os .remove (os .path .join (baseclasses_path , folder ))
102+ except Exception as e :
103+ pass
104+
101105 if platform_type == "win32" :
102106 zip_path = "vendor/windows.zip" # Path inside the package
103107 copy_dependencies (zip_path , "windows.zip" )
@@ -111,6 +115,7 @@ def copy_dependencies(zip_path, file):
111115 sys .path .append ("worlds/dk64/archipelago/" )
112116 sys .path .append ("custom_worlds/dk64.apworld/dk64/" )
113117 sys .path .append ("custom_worlds/dk64.apworld/dk64/archipelago/" )
118+
114119 import randomizer .ItemPool as DK64RItemPool
115120
116121 from randomizer .Enums .Items import Items as DK64RItems
@@ -133,9 +138,11 @@ def copy_dependencies(zip_path, file):
133138 from randomizer .CompileHints import compileMicrohints
134139 from randomizer .Enums .Types import Types
135140 from randomizer .Enums .Kongs import Kongs
141+ from randomizer .Enums .Levels import Levels
136142 from randomizer .Enums .Maps import Maps
137143 from randomizer .Enums .Locations import Locations as DK64RLocations
138- from randomizer .Enums .Settings import WinConditionComplex
144+ from randomizer .Enums .Settings import WinConditionComplex , SwitchsanityLevel
145+ from randomizer .Enums .Switches import Switches
139146 from randomizer .Lists import Item as DK64RItem
140147 from worlds .LauncherComponents import Component , components , Type , icon_paths
141148 import randomizer .ShuffleExits as ShuffleExits
@@ -267,6 +274,16 @@ def generate_early(self):
267274 settings_dict = decrypt_settings_string_enum (self .settings_string )
268275 settings_dict ["archipelago" ] = True
269276 settings_dict ["starting_kongs_count" ] = self .options .starting_kong_count .value
277+ settings_dict ["open_lobbies" ] = self .options .open_lobbies .value
278+ settings_dict ["krool_in_boss_pool" ] = self .options .krool_in_boss_pool .value
279+ settings_dict ["helm_phase_count" ] = self .options .helm_phase_count .value
280+ settings_dict ["krool_phase_count" ] = self .options .krool_phase_count .value
281+ settings_dict ["medal_cb_req" ] = self .options .medal_cb_req .value
282+ settings_dict ["mermaid_gb_pearls" ] = self .options .mermaid_gb_pearls .value
283+ settings_dict ["medal_requirement" ] = self .options .medal_requirement .value
284+ settings_dict ["rareware_gb_fairies" ] = self .options .rareware_gb_fairies .value
285+ settings_dict ["krool_key_count" ] = self .options .krool_key_count .value
286+ settings_dict ["switchsanity" ] = self .options .switchsanity .value
270287 settings_dict ["starting_keys_list_selected" ] = []
271288 for item in self .options .start_inventory :
272289 if item == "Key 1" :
@@ -289,9 +306,29 @@ def generate_early(self):
289306 settings_dict ["win_condition_item" ] = WinConditionComplex .req_key
290307 settings_dict ["win_condition_count" ] = 8
291308 settings = Settings (settings_dict , self .random )
309+ # Set all the static slot data that UT needs to know. Most of these would have already been decided in normal generation by now, so they are just overwritten here.
310+ if hasattr (self .multiworld , "generation_is_fake" ):
311+ if hasattr (self .multiworld , "re_gen_passthrough" ):
312+ if "Donkey Kong 64" in self .multiworld .re_gen_passthrough :
313+ passthrough = self .multiworld .re_gen_passthrough ["Donkey Kong 64" ]
314+ settings .level_order = passthrough ["LevelOrder" ]
315+ settings .starting_kong_list = passthrough ["StartingKongs" ]
316+ settings .BossBananas = passthrough ["BossBananas" ]
317+ settings .boss_maps = passthrough ["BossMaps" ]
318+ settings .boss_kongs = passthrough ["BossKongs" ]
319+ settings .lanky_freeing_kong = passthrough ["LankyFreeingKong" ]
320+ settings .helm_order = passthrough ["HelmOrder" ]
321+ # There's multiple sources of truth for helm order.
322+ settings .helm_donkey = 0 in settings .helm_order
323+ settings .helm_diddy = 4 in settings .helm_order
324+ settings .helm_lanky = 3 in settings .helm_order
325+ settings .helm_tiny = 2 in settings .helm_order
326+ settings .helm_chunky = 1 in settings .helm_order
292327 # We need to set the freeing kongs here early, as they won't get filled in any other part of the AP process
293328 settings .diddy_freeing_kong = self .random .randint (0 , 4 )
294- settings .lanky_freeing_kong = self .random .randint (0 , 4 )
329+ # Lanky freeing kong actually changes logic, so UT should use the slot data rather than genning a new one.
330+ if not hasattr (self .multiworld , "generation_is_fake" ):
331+ settings .lanky_freeing_kong = self .random .randint (0 , 4 )
295332 settings .tiny_freeing_kong = self .random .randint (0 , 4 )
296333 settings .chunky_freeing_kong = self .random .randint (0 , 4 )
297334 spoiler = Spoiler (settings )
@@ -314,7 +351,9 @@ def generate_early(self):
314351 randomize_enemies_0 (spoiler )
315352 # Handle Loading Zones - this will handle LO and (someday?) LZR appropriately
316353 if spoiler .settings .shuffle_loading_zones != ShuffleLoadingZones .none :
317- ShuffleExits .ExitShuffle (spoiler , skip_verification = True )
354+ # UT should not reshuffle the level order, but should update the exits
355+ if not hasattr (self .multiworld , "generation_is_fake" ):
356+ ShuffleExits .ExitShuffle (spoiler , skip_verification = True )
318357 spoiler .UpdateExits ()
319358
320359 def create_regions (self ) -> None :
@@ -549,6 +588,14 @@ def fill_slot_data(self) -> dict:
549588 "FairyRequirement" : self .logic_holder .settings .rareware_gb_fairies ,
550589 "MermaidPearls" : self .logic_holder .settings .mermaid_gb_pearls ,
551590 "JetpacReq" : self .logic_holder .settings .medal_requirement ,
591+ "BossBananas" : ", " .join ([str (cost ) for cost in self .logic_holder .settings .BossBananas ]),
592+ "BossMaps" : ", " .join (map .name for map in self .logic_holder .settings .boss_maps ),
593+ "BossKongs" : ", " .join (kong .name for kong in self .logic_holder .settings .boss_kongs ),
594+ "LankyFreeingKong" : self .logic_holder .settings .lanky_freeing_kong ,
595+ "HelmOrder" : ", " .join ([str (room ) for room in self .logic_holder .settings .helm_order ]),
596+ "OpenLobbies" : self .logic_holder .settings .open_lobbies ,
597+ "KroolInBossPool" : self .logic_holder .settings .krool_in_boss_pool ,
598+ "SwitchSanity" : {switch .name : {"kong" : data .kong .name , "type" : data .switch_type .name } for switch , data in self .logic_holder .settings .switchsanity_data .items ()},
552599 }
553600
554601 def write_spoiler (self , spoiler_handle : typing .TextIO ):
@@ -579,6 +626,13 @@ def write_spoiler(self, spoiler_handle: typing.TextIO):
579626 spoiler_handle .write ("\n " )
580627 spoiler_handle .write ("Removed Barriers: " + ", " .join ([barrier .name for barrier in self .logic_holder .settings .remove_barriers_selected ]))
581628 spoiler_handle .write ("\n " )
629+ if self .logic_holder .settings .switchsanity != SwitchsanityLevel .off :
630+ spoiler_handle .write ("Switchsanity Settings: \n " )
631+ for switch , data in self .logic_holder .settings .switchsanity_data .items ():
632+ if self .logic_holder .settings .switchsanity == SwitchsanityLevel .helm_access :
633+ if switch not in (Switches .IslesHelmLobbyGone , Switches .IslesMonkeyport ):
634+ continue
635+ spoiler_handle .write (f" - { switch .name } : { data .kong .name } with { data .switch_type .name } \n " )
582636 spoiler_handle .write ("Generated Time: " + time .strftime ("%d-%m-%Y %H:%M:%S" , time .gmtime ()) + " GMT" )
583637 spoiler_handle .write ("\n " )
584638 spoiler_handle .write ("Randomizer Version: " + self .logic_holder .settings .version )
@@ -607,3 +661,23 @@ def collect(self, state: CollectionState, item: Item) -> bool:
607661 if change :
608662 self .logic_holder .UpdateFromArchipelagoItems (state )
609663 return change
664+
665+ def interpret_slot_data (self , slot_data : dict [str , any ]) -> dict [str , any ]:
666+ """Parse slot data for any logical bits that need to match the real generation. Used by Universal Tracker."""
667+ # Parse the string data
668+ level_order = slot_data ["LevelOrder" ].split (", " )
669+ starting_kongs = slot_data ["StartingKongs" ].split (", " )
670+ boss_bananas = slot_data ["BossBananas" ].split (", " )
671+ boss_maps = slot_data ["BossMaps" ].split (", " )
672+ boss_kongs = slot_data ["BossKongs" ].split (", " )
673+ helm_order = slot_data ["HelmOrder" ].split (", " )
674+
675+ relevant_data = {}
676+ relevant_data ["LevelOrder" ] = dict (enumerate ([Levels [level ] for level in level_order ], start = 1 ))
677+ relevant_data ["StartingKongs" ] = [Kongs [kong ] for kong in starting_kongs ]
678+ relevant_data ["BossBananas" ] = [int (cost ) for cost in boss_bananas ]
679+ relevant_data ["BossMaps" ] = [Maps [map ] for map in boss_maps ]
680+ relevant_data ["BossKongs" ] = [Kongs [kong ] for kong in boss_kongs ]
681+ relevant_data ["LankyFreeingKong" ] = slot_data ["LankyFreeingKong" ]
682+ relevant_data ["HelmOrder" ] = [int (room ) for room in helm_order ]
683+ return relevant_data
0 commit comments