forked from ArchipelagoMW/Archipelago
- Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathMain.py
More file actions
Latest commit
390 lines (320 loc) · 19.8 KB
/
Copy pathMain.py
File metadata and controls
390 lines (320 loc) · 19.8 KB
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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
importcollections
fromcollections.abcimportMapping
importconcurrent.futures
importlogging
importos
importtempfile
importtime
fromtypingimportAny
importzipfile
importzlib
importworlds
fromBaseClassesimportCollectionState, Item, Location, LocationProgressType, MultiWorld
fromFillimportFillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \
parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
fromNetUtilsimportconvert_to_base_types
fromOptionsimportStartInventoryPool
fromUtilsimport__version__, output_path, restricted_dumps, version_tuple
fromsettingsimportget_settings
fromworldsimportAutoWorld
fromworlds.generic.Rulesimportexclusion_rules, locality_rules
__all__= ["main"]
defmain(args, seed=None, baked_server_options: dict[str, object] |None=None):
ifnotbaked_server_options:
baked_server_options=get_settings().server_options.as_dict()
assertisinstance(baked_server_options, dict)
ifargs.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path=args.outputpath
start=time.perf_counter()
# initialize the multiworld
multiworld=MultiWorld(args.multi)
logger=logging.getLogger()
multiworld.set_seed(seed, args.race, str(args.outputname) ifargs.outputnameelseNone)
multiworld.plando_options=args.plando
multiworld.game=args.game.copy()
multiworld.player_name=args.name.copy()
multiworld.sprite=args.sprite.copy()
multiworld.sprite_pool=args.sprite_pool.copy()
multiworld.set_options(args)
ifargs.csv_output:
fromOptionsimportdump_player_options
dump_player_options(multiworld)
multiworld.set_item_links()
multiworld.state=CollectionState(multiworld)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name=max(len(text) fortextinAutoWorld.AutoWorldRegister.world_types)
world_classes=AutoWorld.AutoWorldRegister.world_types.values()
version_count=max(len(cls.world_version.as_simple_string()) forclsinworld_classes)
item_count=len(str(max(len(cls.item_names) forclsinworld_classes)))
location_count=len(str(max(len(cls.location_names) forclsinworld_classes)))
forname, clsinAutoWorld.AutoWorldRegister.world_types.items():
ifnotcls.hiddenandlen(cls.item_names) >0:
logger.info(f" {name:{longest_name}}: "
f"v{cls.world_version.as_simple_string():{version_count}} | "
f"Items: {len(cls.item_names):{item_count}} | "
f"Locations: {len(cls.location_names):{location_count}}")
delitem_count, location_count
# This assertion method should not be necessary to run if we are not outputting any multidata.
ifnotargs.skip_outputandnotargs.spoiler_only:
AutoWorld.call_stage(multiworld, "assert_generate")
AutoWorld.call_all(multiworld, "generate_early")
logger.info('')
forplayerinmultiworld.player_ids:
foritem_name, countinmultiworld.worlds[player].options.start_inventory.value.items():
for_inrange(count):
multiworld.push_precollected(multiworld.create_item(item_name, player))
foritem_name, countingetattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.items():
for_inrange(count):
multiworld.push_precollected(multiworld.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
early=multiworld.early_items[player].get(item_name, 0)
ifearly:
multiworld.early_items[player][item_name] =max(0, early-count)
remaining_count=count-early
ifremaining_count>0:
local_early=multiworld.local_early_items[player].get(item_name, 0)
iflocal_early:
multiworld.early_items[player][item_name] =max(0, local_early-remaining_count)
dellocal_early
delearly
# items can't be both local and non-local, prefer local
multiworld.worlds[player].options.non_local_items.value-=multiworld.worlds[player].options.local_items.value
multiworld.worlds[player].options.non_local_items.value-=set(multiworld.local_early_items[player])
# Clear non-applicable local and non-local items.
ifmultiworld.players==1:
multiworld.worlds[1].options.non_local_items.value=set()
multiworld.worlds[1].options.local_items.value=set()
logger.info('Creating MultiWorld.')
AutoWorld.call_all(multiworld, "create_regions")
logger.info('Creating Items.')
AutoWorld.call_all(multiworld, "create_items")
logger.info('Calculating Access Rules.')
AutoWorld.call_all(multiworld, "set_rules")
forplayerinmultiworld.player_ids:
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
multiworld.worlds[player].options.priority_locations.value-=multiworld.worlds[player].options.exclude_locations.value
world_excluded_locations=set()
forlocation_nameinmultiworld.worlds[player].options.priority_locations.value:
try:
location=multiworld.get_location(location_name, player)
exceptKeyError:
continue
iflocation.progress_type!=LocationProgressType.EXCLUDED:
location.progress_type=LocationProgressType.PRIORITY
else:
logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.")
world_excluded_locations.add(location_name)
multiworld.worlds[player].options.priority_locations.value-=world_excluded_locations
# Set local and non-local item rules.
# This function is called so late because worlds might otherwise overwrite item_rules which are how locality works
ifmultiworld.players>1:
locality_rules(multiworld)
multiworld.plando_item_blocks=parse_planned_blocks(multiworld)
AutoWorld.call_all(multiworld, "connect_entrances")
AutoWorld.call_all(multiworld, "generate_basic")
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
fallback_inventory=StartInventoryPool({})
depletion_pool: dict[int, dict[str, int]] = {
player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy()
forplayerinmultiworld.player_ids
}
target_per_player= {
player: sum(target_items.values()) forplayer, target_itemsindepletion_pool.items() iftarget_items
}
iftarget_per_player:
new_itempool: list[Item] = []
# Make new itempool with start_inventory_from_pool items removed
foriteminmultiworld.itempool:
ifdepletion_pool[item.player].get(item.name, 0):
depletion_pool[item.player][item.name] -=1
else:
new_itempool.append(item)
# Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool
forplayer, targetintarget_per_player.items():
unfound_items= {item: countforitem, countindepletion_pool[player].items() ifcount}
ifunfound_items:
player_name=multiworld.get_player_name(player)
logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}")
needed_items=target_per_player[player] -sum(unfound_items.values())
new_itempool+= [multiworld.worlds[player].create_filler() for_inrange(needed_items)]
assertlen(multiworld.itempool) ==len(new_itempool), "Item Pool amounts should not change."
multiworld.itempool[:] =new_itempool
multiworld.link_items()
ifany(world.options.item_linksforworldinmultiworld.worlds.values()):
multiworld._all_state=None
logger.info("Running Item Plando.")
resolve_early_locations_for_planned(multiworld)
distribute_planned_blocks(multiworld, [xforplayerinmultiworld.plando_item_blocks
forxinmultiworld.plando_item_blocks[player]])
logger.info('Running Pre Main Fill.')
AutoWorld.call_all(multiworld, "pre_fill")
logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.')
ifmultiworld.algorithm=='flood':
flood_items(multiworld) # different algo, biased towards early game progress items
elifmultiworld.algorithm=='balanced':
distribute_items_restrictive(multiworld, get_settings().generator.panic_method)
AutoWorld.call_all(multiworld, 'post_fill')
ifmultiworld.players>1andnotargs.skip_prog_balancing:
balance_multiworld_progression(multiworld)
else:
logger.info("Progression balancing skipped.")
AutoWorld.call_all(multiworld, "finalize_multiworld")
AutoWorld.call_all(multiworld, "pre_output")
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
multiworld.random.passthrough=False
ifargs.skip_output:
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() -start)
returnmultiworld
logger.info(f'Beginning output...')
outfilebase='AP_'+multiworld.seed_name
ifargs.spoiler_only:
ifargs.spoiler>1:
logger.info('Calculating playthrough.')
multiworld.spoiler.create_playthrough(create_paths=args.spoiler>2)
multiworld.spoiler.to_file(output_path('%s_Spoiler.txt'%outfilebase))
logger.info('Done. Skipped multidata modification. Total time: %s', time.perf_counter() -start)
returnmultiworld
output=tempfile.TemporaryDirectory()
withoutputastemp_dir:
output_players= [playerforplayerinmultiworld.player_idsifAutoWorld.World.generate_output.__code__
isnotmultiworld.worlds[player].generate_output.__code__]
withconcurrent.futures.ThreadPoolExecutor(len(output_players) +2) aspool:
check_accessibility_task=pool.submit(multiworld.fulfills_accessibility)
output_file_futures= [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)]
forplayerinoutput_players:
# skip starting a thread for methods that say "pass".
output_file_futures.append(
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
# collect ER hint info
er_hint_data: dict[int, dict[int, str]] = {}
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
defwrite_multidata():
importNetUtils
fromNetUtilsimportHintStatus
slot_data: dict[int, Mapping[str, Any]] = {}
client_versions: dict[int, tuple[int, int, int]] = {}
games: dict[int, str] = {}
minimum_versions: NetUtils.MinimumVersions= {
"server": AutoWorld.World.required_server_version, "clients": client_versions
}
slot_info: dict[int, NetUtils.NetworkSlot] = {}
names= [[nameforplayer, nameinsorted(multiworld.player_name.items())]]
forslotinmultiworld.player_ids:
player_world: AutoWorld.World=multiworld.worlds[slot]
minimum_versions["server"] =max(minimum_versions["server"], player_world.required_server_version)
client_versions[slot] =player_world.required_client_version
games[slot] =multiworld.game[slot]
slot_info[slot] =NetUtils.NetworkSlot(names[0][slot-1], multiworld.game[slot],
multiworld.player_types[slot])
forslot, groupinmultiworld.groups.items():
games[slot] =multiworld.game[slot]
slot_info[slot] =NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot],
group_members=sorted(group["players"]))
precollected_items= {player: [item.codeforiteminworld_precollectediftype(item.code) ==int]
forplayer, world_precollectedinmultiworld.precollected_items.items()}
precollected_hints: dict[int, set[NetUtils.Hint]] = {
player: set() forplayerinrange(1, multiworld.players+1+len(multiworld.groups))
}
forslotinmultiworld.player_ids:
slot_data[slot] =multiworld.worlds[slot].fill_slot_data()
defprecollect_hint(location: Location, auto_status: HintStatus):
entrance=er_hint_data.get(location.player, {}).get(location.address, "")
hint=NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False, entrance, location.item.flags, auto_status)
precollected_hints[location.player].add(hint)
iflocation.item.playernotinmultiworld.groups:
precollected_hints[location.item.player].add(hint)
else:
forplayerinmultiworld.groups[location.item.player]["players"]:
precollected_hints[player].add(hint)
locations_data: dict[int, dict[int, tuple[int, int, int]]] = {player: {} forplayerinmultiworld.player_ids}
forlocationinmultiworld.get_filled_locations():
iftype(location.address) ==int:
assertlocation.item.codeisnotNone, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}, Item: {location.item}"
assertlocation.addressnotinlocations_data[location.player], (
f"Locations with duplicate address. {location} and "
f"{locations_data[location.player][location.address]}")
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
auto_status=HintStatus.HINT_AVOIDiflocation.item.trapelseHintStatus.HINT_PRIORITY
iflocation.nameinmultiworld.worlds[location.player].options.start_location_hints:
ifnotlocation.item.trap: # Unspecified status for location hints, except traps
auto_status=HintStatus.HINT_UNSPECIFIED
precollect_hint(location, auto_status)
eliflocation.item.nameinmultiworld.worlds[location.item.player].options.start_hints:
precollect_hint(location, auto_status)
elifany([location.item.nameinmultiworld.worlds[player].options.start_hints
forplayerinmultiworld.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location, auto_status)
# embedded data package
data_package= {
game_world.game: worlds.network_data_package["games"][game_world.game]
forgame_worldinmultiworld.worlds.values()
}
data_package["Archipelago"] =worlds.network_data_package["games"]["Archipelago"]
checks_in_area: dict[int, dict[str, int|list[int]]] = {}
# get spheres -> filter address==None -> skip empty
spheres: list[dict[int, set[int]]] = []
forsphereinmultiworld.get_sendable_spheres():
current_sphere: dict[int, set[int]] =collections.defaultdict(set)
forsphere_locationinsphere:
current_sphere[sphere_location.player].add(sphere_location.address)
ifcurrent_sphere:
spheres.append(dict(current_sphere))
multidata: NetUtils.MultiData= {
"slot_data": slot_data,
"slot_info": slot_info,
"connect_names": {name: (0, player) forplayer, nameinmultiworld.player_name.items()},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": baked_server_options,
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"precollected_hints": precollected_hints,
"version": (version_tuple.major, version_tuple.minor, version_tuple.build),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": multiworld.seed_name,
"spheres": spheres,
"datapackage": data_package,
"race_mode": int(multiworld.is_race),
}
# TODO: change to `"version": version_tuple` after getting better serialization
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
forkeyin ("slot_data", "er_hint_data"):
multidata[key] =convert_to_base_types(multidata[key])
serialized_multidata=zlib.compress(restricted_dumps(multidata), 9)
withopen(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') asf:
f.write(bytes([3])) # version of format
f.write(serialized_multidata)
output_file_futures.append(pool.submit(write_multidata))
ifnotcheck_accessibility_task.result():
ifnotmultiworld.can_beat_game():
raiseFillError("Game appears as unbeatable. Aborting.", multiworld=multiworld)
else:
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occurred.
fori, futureinenumerate(concurrent.futures.as_completed(output_file_futures), start=1):
ifi%10==0ori==len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
future.result()
ifargs.spoiler>1:
logger.info('Calculating playthrough.')
multiworld.spoiler.create_playthrough(create_paths=args.spoiler>2)
ifargs.spoiler:
multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt'%outfilebase))
zipfilename=output_path(f"AP_{multiworld.seed_name}.zip")
logger.info(f"Creating final archive at {zipfilename}")
withzipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) aszf:
forfileinos.scandir(temp_dir):
zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() -start)
returnmultiworld