forked from AmazingAmpharos/OoT-Randomizer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Main.py
287 lines (233 loc) · 11 KB
/
Main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
from collections import OrderedDict
from itertools import zip_longest
import json
import logging
import platform
import random
import subprocess
import time
import os, os.path
import sys
import struct
from BaseClasses import World, CollectionState, Item, Spoiler
from EntranceShuffle import link_entrances
from Rom import LocalRom
from Patches import patch_rom
from Regions import create_regions
from Dungeons import create_dungeons
from Rules import set_rules
from Fill import distribute_items_restrictive
from ItemList import generate_itempool
from Hints import buildGossipHints
from Utils import default_output_path, is_bundled, subprocess_args
from version import __version__
from OcarinaSongs import verify_scarecrow_song_str
class dummy_window():
def __init__(self):
pass
def update_status(self, text):
pass
def update_progress(self, val):
pass
def main(settings, window=dummy_window()):
"""
:param settings:
:param window:
:return:
"""
start = time.clock()
logger = logging.getLogger('')
# verify that the settings are valid
if settings.free_scarecrow:
verify_scarecrow_song_str(settings.scarecrow_song, settings.ocarina_songs)
# initialize the world
worlds = []
if settings.compress_rom == 'None':
settings.create_spoiler = True
settings.update()
if not settings.world_count:
settings.world_count = 1
if settings.world_count < 1 or settings.world_count > 31:
raise Exception('World Count must be between 1 and 31')
if settings.player_num > settings.world_count or settings.player_num < 1:
raise Exception('Player Num must be between 1 and %d' % settings.world_count)
for i in range(0, settings.world_count):
worlds.append(World(settings))
random.seed(worlds[0].numeric_seed)
logger.info('OoT Randomizer Version %s - Seed: %s\n\n', __version__, worlds[0].seed)
# we load the rom before creating the seed so that error get caught early
if settings.compress_rom != 'None':
window.update_status('Loading ROM')
rom = LocalRom(settings)
window.update_status('Creating the Worlds')
for id, world in enumerate(worlds):
world.id = id
logger.info('Generating World %d.' % id)
world.spoiler = Spoiler(worlds)
window.update_progress(0 + (((id + 1) / settings.world_count) * 1))
logger.info('Creating Overworld')
if world.quest == 'master':
for dungeon in world.dungeon_mq:
world.dungeon_mq[dungeon] = True
elif world.quest == 'mixed':
for dungeon in world.dungeon_mq:
world.dungeon_mq[dungeon] = random.choice([True, False])
else:
for dungeon in world.dungeon_mq:
world.dungeon_mq[dungeon] = False
create_regions(world)
window.update_progress(0 + (((id + 1) / settings.world_count) * 2))
logger.info('Creating Dungeons')
create_dungeons(world)
window.update_progress(0 + (((id + 1) / settings.world_count) * 3))
logger.info('Linking Entrances')
link_entrances(world)
if settings.shopsanity != 'off':
world.random_shop_prices()
window.update_progress(0 + (((id + 1) / settings.world_count) * 4))
logger.info('Calculating Access Rules.')
set_rules(world)
window.update_progress(0 + (((id + 1) / settings.world_count) * 5))
logger.info('Generating Item Pool.')
generate_itempool(world)
window.update_status('Placing the Items')
logger.info('Fill the world.')
distribute_items_restrictive(window, worlds)
window.update_progress(35)
if settings.create_spoiler:
window.update_status('Calculating Spoiler Data')
logger.info('Calculating playthrough.')
create_playthrough(worlds)
window.update_progress(50)
if settings.hints != 'none':
window.update_status('Calculating Hint Data')
CollectionState.update_required_items(worlds)
buildGossipHints(worlds[settings.player_num - 1])
window.update_progress(55)
logger.info('Patching ROM.')
if settings.world_count > 1:
outfilebase = 'OoT_%s_%s_W%dP%d' % (worlds[0].settings_string, worlds[0].seed, worlds[0].world_count, worlds[0].player_num)
else:
outfilebase = 'OoT_%s_%s' % (worlds[0].settings_string, worlds[0].seed)
output_dir = default_output_path(settings.output_dir)
if settings.compress_rom != 'None':
window.update_status('Patching ROM')
patch_rom(worlds[settings.player_num - 1], rom)
window.update_progress(65)
rom_path = os.path.join(output_dir, '%s.z64' % outfilebase)
window.update_status('Saving Uncompressed ROM')
rom.write_to_file(rom_path)
if settings.compress_rom == 'True':
window.update_status('Compressing ROM')
logger.info('Compressing ROM.')
if is_bundled():
compressor_path = "."
else:
compressor_path = "Compress"
if platform.system() == 'Windows':
if 8 * struct.calcsize("P") == 64:
compressor_path += "\\Compress.exe"
else:
compressor_path += "\\Compress32.exe"
elif platform.system() == 'Linux':
compressor_path += "/Compress"
elif platform.system() == 'Darwin':
compressor_path += "/Compress.out"
else:
compressor_path = ""
logger.info('OS not supported for compression')
if compressor_path != "":
run_process(window, logger, [compressor_path, rom_path, os.path.join(output_dir, '%s-comp.z64' % outfilebase)])
os.remove(rom_path)
window.update_progress(95)
if settings.create_spoiler:
window.update_status('Creating Spoiler Log')
worlds[settings.player_num - 1].spoiler.to_file(os.path.join(output_dir, '%s_Spoiler.txt' % outfilebase))
window.update_progress(100)
window.update_status('Success: Rom patched successfully')
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.clock() - start)
return worlds[settings.player_num - 1]
def run_process(window, logger, args):
"""
:param window:
:param logger:
:param args:
:return:
"""
process = subprocess.Popen(args, **subprocess_args(True))
filecount = None
while True:
line = process.stdout.readline()
if line != b'':
find_index = line.find(b'files remaining')
if find_index > -1:
files = int(line[:find_index].strip())
if filecount == None:
filecount = files
window.update_progress(65 + ((1 - (files / filecount)) * 30))
logger.info(line.decode('utf-8').strip('\n'))
else:
break
def create_playthrough(worlds):
"""
:param worlds:
:return:
"""
if worlds[0].check_beatable_only and not CollectionState.can_beat_game([world.state for world in worlds]):
raise RuntimeError('Uncopied is broken too.')
# create a copy as we will modify it
old_worlds = worlds
worlds = [world.copy() for world in worlds]
# if we only check for beatable, we can do this sanity check first before writing down spheres
if worlds[0].check_beatable_only and not CollectionState.can_beat_game([world.state for world in worlds]):
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
state_list = [world.state for world in worlds]
# Get all item locations in the worlds
required_locations = []
item_locations = [location for state in state_list for location in state.world.get_filled_locations() if location.item.advancement]
# in the first phase, we create the generous spheres. Collecting every item in a sphere will
# mean that every item in the next sphere is collectable. Will contain every reachable item
logging.getLogger('').debug('Building up collection spheres.')
# will loop if there is more items opened up in the previous iteration. Always run once
reachable_items_locations = True
while reachable_items_locations:
# get reachable new items locations
reachable_items_locations = [location for location in item_locations if location.name not in state_list[location.world.id].collected_locations and state_list[location.world.id].can_reach(location)]
for location in reachable_items_locations:
# Mark the location collected in the state world it exists in
state_list[location.world.id].collected_locations[location.name] = True
# Collect the item for the state world it is for
state_list[location.item.world.id].collect(location.item)
required_locations.append(location)
# in the second phase, we cull each sphere such that the game is still beatable, reducing each
# range of influence to the bare minimum required inside it. Effectively creates a min play
for location in reversed(required_locations):
# we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s is required to beat the game.', location.item.name)
old_item = location.item
# Uncollect the item location. Removing it from the collected_locations
# will ensure that can_beat_game will try to collect it if it can.
# Because we search in reverse sphere order, all the later spheres will
# have their locations flagged to be re-searched.
location.item = None
state_list[old_item.world.id].remove(old_item)
del state_list[location.world.id].collected_locations[location.name]
# remove the item from the world and test if the game is still beatable
if CollectionState.can_beat_game(state_list):
# cull entries for spoiler walkthrough at end
required_locations.remove(location)
else:
# still required, got to keep it around
location.item = old_item
# This ensures the playthrough shows items being collected in the proper order.
collection_spheres = []
while required_locations:
sphere = [location for location in required_locations if state_list[location.world.id].can_reach(location)]
for location in sphere:
required_locations.remove(location)
state_list[location.item.world.id].collect(location.item)
collection_spheres.append(sphere)
# we can finally output our playthrough
for world in old_worlds:
world.spoiler.playthrough = OrderedDict([(str(i + 1), {location: location.item for location in sphere}) for i, sphere in enumerate(collection_spheres)])