Compare commits

..

101 Commits
0.0.2 ... 0.1.0

Author SHA1 Message Date
Fabian Dill
4813fcac08 include version tuple in manifest 2021-05-10 12:02:18 +02:00
Fabian Dill
e50db61030 constrict Factorio logic to require all paths to a product, not any.
Should narrow this down in a careful manner later.
2021-05-10 02:33:54 +02:00
Fabian Dill
f8c3b695d0 Add Swedish Minecraft Tutorial by Albinum 2021-05-10 01:46:42 +02:00
Edos512
431b64c574 Merge Tutorials from BM 2021-05-10 01:38:26 +02:00
Fabian Dill
f06d160615 don't check the AP tag anymore 2021-05-10 01:18:57 +02:00
Fabian Dill
909172cbad Factorio, Minecraft & Hollow Knight: add startinventory support 2021-05-09 21:22:21 +02:00
Fabian Dill
382c6d0445 Factorio: Fix free samples still preventing starting items 2021-05-09 20:39:42 +02:00
Fabian Dill
4efd27694a Factorio: no longer lock starting_items behind free_samples 2021-05-09 18:13:29 +02:00
KonoTyran
fa24fd31d0 re-added stuff (#7)
* add minecraft randomizer usage/installation tutorial

* update tutorial to specify Java edition of Minecraft to avoid confusion.

* fixed some grammar and spelling in the minecraft_en.md tutorial

* add minecraft randomizer usage/installation tutorial

* update tutorial to specify Java edition of Minecraft to avoid confusion.

* fixed some grammar and spelling in the minecraft_en.md tutorial

* update readme to relfect removal of name field from connect command.

* re-added explenations in yaml manualy wrapped the lines to stop them from looking weird on the web page.
2021-05-09 18:06:51 +02:00
Fabian Dill
c55983af5f Factorio: add starting_items 2021-05-09 17:46:26 +02:00
Fabian Dill
9c3d12dc55 Factorio: Embed slot name into mod 2021-05-09 17:26:53 +02:00
Fabian Dill
37755cd362 Factorio: Automatically find and force create bridge file 2021-05-09 16:49:47 +02:00
KonoTyran
eb02d65dbb minecraft setup tutorial changes. (#6)
* add minecraft randomizer usage/installation tutorial

* update tutorial to specify Java edition of Minecraft to avoid confusion.

* fixed some grammar and spelling in the minecraft_en.md tutorial

* add minecraft randomizer usage/installation tutorial

* update tutorial to specify Java edition of Minecraft to avoid confusion.

* fixed some grammar and spelling in the minecraft_en.md tutorial

* update readme to relfect removal of name field from connect command.
2021-05-08 19:51:12 +02:00
Fabian Dill
4d38f44da3 fix defaults for Factorio and Minecraft 2021-05-08 19:11:08 +02:00
Fabian Dill
212abc2b5a increment version 2021-05-08 13:42:18 +02:00
Fabian Dill
3797c20488 update README.md 2021-05-08 13:40:23 +02:00
espeon65536
2f7e532f4f Minecraft Randomizer
Squash merge, original Commits:

* Minecraft locations, items, and generation without logic

* added id lookup for minecraft

* typing import fix in minecraft/Items.py

* fix 2

* implementing Minecraft options and hard/postgame advancement exclusion

* first logic pass (75/80)

* logic pass 2 and proper completion conditions

* added insane difficulty pool, modified method of excluding item pools for easier extension

* bump network_data_package version

* minecraft testing framework

* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item

* Testing now functions, split tests up by advancement pane, added some story tests

* Newer testing framework: every advancement gets its own function, for ease of testing

* fixed logic for The End... Again...

* changed option names to "include_hard_advancements" etc.

* village/pillager-related advancements now require can_adventure: weapon + food

* a few minecraft tests

* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name

* additional MC tests

* more tests, mostly nether-related tests

* more tests, removed anvil path for Two Birds One Arrow

* include Minecraft slot data, and a world seed for each Minecraft player slot

* Added new items: ender pearls, lapis, porkchops

* All remaining Minecraft tests

* formatting of Minecraft tests and logic for better readability

* require Wither kill for Monsters Hunted

* properly removed 8 Emeralds item from item pool

* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill

* Added 12 new advancements (ported from old achievement system)

* renamed "On a Rail" for consistency with modern advancements

* tests for the new advancements

* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data

* output minecraft options in the spoiler log

* modified advancement goal values for new advancements

* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars

* fixed glowstone block logic for Not Quite Nine Lives

* setup for shuffling MC structures: building ER world and shuffling regions/entrances

* ensured Nether Fortresses can't be placed in the End

* finished logic for structure randomization

* fixed nonnative items always showing up as Hammers in ALttP shops

* output minecraft structure info in the spoiler

* generate .apmc file for communication with MC client

* fixed structure rando always using the same seed

* move stuff to worlds/minecraft/Regions.py

* make output apmc file have consistent name with other files

* added minecraft bottle macro; fixed tests imports

* generalizing MC region generation

* restructured structure shuffling in preparation for structure plando

* only output structure rando info in spoiler if they are shuffled

* Force structure rando to always be off, for the stable release

* added Minecraft options to player settings

* formally added combat_difficulty as an option

* Added Ender Dragon into playthrough, cleaned up goal map

* Added new difficulties: Easy, Normal, Hard combat

* moved .apmc generation time to prevent outputs on failed generation

* updated tests for new combat logic

* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix

* moved all MC-specific functions into gen_minecraft

* renamed "logic_version" to "client_version"

* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures

* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching

* added seed_name, player_name, client_version to apmc file

* reenabled structure shuffle

* added entrance tests for minecraft

Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 13:38:57 +02:00
Fabian Dill
eb2a3009f4 Add dungeonscrossed to sample yaml 2021-05-08 12:08:55 +02:00
Fabian Dill
5c9aa09c80 LttP: implement dungeonscrossed ER 2021-05-08 12:04:03 +02:00
Fabian Dill
e5bbcb8d27 merge of 6bf02455e6 2021-05-08 01:49:30 +02:00
Fabian Dill
cf488e5a5d convert parse_zspr to staticmethod 2021-05-08 01:44:37 +02:00
Fabian Dill
36ef9e8a72 LttP: Read author game display name from zspr 2021-05-07 22:34:02 +02:00
Fabian Dill
e7d254aed7 Factorio: make automation science pack rocket recipe consistent with the others 2021-05-07 22:09:04 +02:00
Fabian Dill
298f2f652a Factorio: add rocket recipe scaling by max science 2021-05-07 21:58:46 +02:00
Fabian Dill
5cb2689609 LttP: speed up ER shuffling caves 2021-05-07 21:01:13 +02:00
Fabian Dill
9ab5ec426d remove leftover from when recipes were events 2021-05-06 12:53:57 +02:00
Fabian Dill
77d3bf9172 Factorio: Allow assembling machine 1 to use fluids
Should improve the flow of the game a bit, no longer having to wait for automation-2 to get started with rocket fuel, processing units and others.
2021-05-03 18:06:21 +02:00
Fabian Dill
328f132498 include player name in factorio mod name. 2021-05-03 16:29:05 +02:00
Fabian Dill
de2ead3a9b Embed Minecraft tutorial in menu 2021-05-03 12:30:09 +02:00
KonoTyran
ff96b391b9 Mincraft Tutorial (#4)
* add minecraft randomizer usage/installation tutorial

* update tutorial to specify Java edition of Minecraft to avoid confusion.
2021-05-03 12:26:35 +02:00
Fabian Dill
4a75d27261 don't precollect Hollow Knight items 2021-05-01 02:16:19 +02:00
Chris Wilson
05e464e379 Update my copyright notice in the LICENSE 2021-04-29 21:09:26 -04:00
Chris Wilson
e8e141b206 Update copyright year on the WebHost to 2021 2021-04-29 20:09:10 -04:00
Fabian Dill
bed8fe82cf speed up can_beat_game 2021-04-29 09:54:49 +02:00
Fabian Dill
3a1d33f499 Allow removing non-LttP Progressive items from CollectionState 2021-04-29 08:25:06 +02:00
Fabian Dill
6ea68dd290 Count non-LttP Progressive items during playthrough creation
(there seem to be more problems hiding though)
2021-04-29 08:14:09 +02:00
Fabian Dill
97030590c2 Factorio: send goal completion 2021-04-29 04:34:47 +02:00
Fabian Dill
60f64cc46b rename get_location_name_from_address to get_location_name_from_id 2021-04-28 15:48:11 +02:00
KonoTyran
5087b78c28 fixed !missing to point to location table not item. (#2) 2021-04-28 15:46:54 +02:00
Fabian Dill
95358bc523 Never download a sprite with Author "Nintendo" 2021-04-28 10:31:24 +02:00
Fabian Dill
4fc1ce77ac only build vanilla sprite data once correctly 2021-04-28 02:39:55 +02:00
Fabian Dill
b8c7d6a72f remove remaining sprite data 2021-04-27 07:19:53 +02:00
Fabian Dill
e04fbd1d77 update setup for newer cx_Freeze #2 2021-04-27 05:26:27 +02:00
Fabian Dill
fb8229fda5 update setup for newer cx_Freeze 2021-04-25 04:02:38 +02:00
Fabian Dill
569e0e3004 Factorio: add option: random tech ingredients 2021-04-24 01:16:49 +02:00
Fabian Dill
73ed18c11d Move update_sprites as --update_sprites to LttPAdjuster 2021-04-21 23:53:59 +02:00
Daniel Grace
65df153947 Overhauls control.lua template (#1)
* Overhauls control.lua template

- Adds buffering of received free-samples items.
  - Players with a full inventory will receive new items as space becomes
    available.
  - Players who do not yet exist in the world will receive all free samples
    upon joining.

- When receiving new technologies, announce the technology BEFORE marking
  it as researched so that it appears before its free samples in the log.

- If receiving a half-stack of free samples, use math.ceil on the division
  in case a mod uses an odd number as a stack size... or a stack size of 1

- Handle free_samples logic in the Lua side rather than as part of a Jinja
  template.  This makes this hopefully more adaptable to a future setup
  where all the rando information is shipped as startup settings.

* Apparently, I'm supposed to give myself credit.  Or something.
2021-04-18 06:06:53 +02:00
Fabian Dill
2dd6dcab20 remote now unneccessary line 2021-04-17 22:01:34 +02:00
Fabian Dill
4494207717 generalize LocationScout and make LttP remote items break entirely 2021-04-17 22:00:45 +02:00
Fabian Dill
88265c5585 flatten and integer cast HK options per request 2021-04-17 21:16:09 +02:00
Fabian Dill
501c55cc26 add per-slot data and embed HK options in it 2021-04-17 21:03:57 +02:00
Fabian Dill
a5efed83b9 Set non-LttP Shop prices to 5 to 140 Rupee range 2021-04-16 21:41:19 +02:00
Fabian Dill
e7a746c06c fix unittest local path 2021-04-15 04:01:25 +02:00
Fabian Dill
063997610d use rich text for received technologies 2021-04-14 18:38:06 +02:00
Fabian Dill
432ae5865d Factorio: Filter bridged technologies correctly
Turns out, lua does not use regex, nor simple string matching, but its own invention.
2021-04-14 17:51:11 +02:00
Fabian Dill
73bc5fb376 restore sanity check for exported technologies 2021-04-14 04:19:44 +02:00
Fabian Dill
a7c9474a37 Factorio: fix sending 2021-04-14 04:14:37 +02:00
Fabian Dill
0cf9baef4b Factorio: add document visibility option to playerSettings.yaml 2021-04-14 02:45:36 +02:00
Fabian Dill
6a06117786 Factorio: use game.print 2021-04-13 20:09:26 +02:00
Fabian Dill
ee30914b2c Send AP text into Factorio worlds 2021-04-13 14:49:32 +02:00
Fabian Dill
a995627e98 Factorio: default to visible tech tree 2021-04-13 12:38:39 +02:00
Fabian Dill
7884c6cd97 Factorio: add mod thumbnail.png 2021-04-13 12:36:40 +02:00
Fabian Dill
4fe10b88b3 Factorio: move info dump to https://mods.factorio.com/mod/archipelago-extractor 2021-04-13 12:35:42 +02:00
Fabian Dill
b7327138f3 Factorio: Show item source and enable research queue 2021-04-13 11:14:05 +02:00
Fabian Dill
433981fd3d pass explicit seed_name from MultiMystery.py 2021-04-12 09:45:07 +02:00
Fabian Dill
2df7e4e537 add seed_name to multidata and RoomInfo 2021-04-12 09:36:45 +02:00
Fabian Dill
764e6e7926 Fix MultiTracker breaking after Hint is used 2021-04-12 00:06:27 +02:00
Fabian Dill
4292cdddd5 Factorio: add Funnel tech shape 2021-04-11 18:19:47 +02:00
Fabian Dill
9aef76767a Include example trigger for legacy weapons 2021-04-11 15:53:13 +02:00
Fabian Dill
3858a12f26 MultiServer: check for correct game 2021-04-10 21:08:01 +02:00
Fabian Dill
1943586221 Factorio: add medium_diamonds and pyramid tech tree layouts 2021-04-10 19:34:30 +02:00
Fabian Dill
6d15aef88a Factorio: align tech tree sections in growing ingredient requirements 2021-04-10 18:45:11 +02:00
Chris Wilson
50f06c3aac Add "swords" option back to playerSettings.yaml 2021-04-10 12:31:04 -04:00
Fabian Dill
7f3c46dd8a Factorio: Allow connecting and entering slot name in one command; /connect server_address slot_name 2021-04-10 15:29:56 +02:00
Fabian Dill
ea15f221ae various fixes to WebHost 2021-04-10 15:26:30 +02:00
Fabian Dill
d4b422840a Fix dynamic world attributes not updating 2021-04-10 06:36:06 +02:00
Fabian Dill
0586b24579 Factorio: add small_diamonds tech tree layout 2021-04-10 03:03:46 +02:00
Fabian Dill
e11016b0a2 fix _done having ingredient letters instead of starting name 2021-04-10 00:21:56 +02:00
Fabian Dill
74a368458e dynamically mark advancement technologies 2021-04-10 00:17:55 +02:00
Fabian Dill
1b70d485c0 shortcut logic for requirement-less technologies 2021-04-10 00:08:59 +02:00
Fabian Dill
2355f9c8d3 Only apply logic for allowed science pack 2021-04-09 22:16:55 +02:00
Fabian Dill
ceea55e3c6 traverse recipe tree for Factorio logic 2021-04-09 22:10:04 +02:00
Fabian Dill
c4d6ac50be turn weapons into boolean swordless 2021-04-09 20:40:45 +02:00
Fabian Dill
4461cb67f0 fix ap-sync and remove infinite techs from randomization 2021-04-09 00:33:32 +02:00
Fabian Dill
f0a6b5a8e4 Factorio:
add visibility option
fix tech_cost using the wrong variable name
fix yaml defaults not init'ing the Option class
LttP:
fix potential pathing confusion in maseya palette shuffler
Server:
Minimum version per team made no sense, removed
2021-04-08 19:53:24 +02:00
Fabian Dill
443fc03700 Send actual NetworkPlayer on Connected too 2021-04-07 02:49:36 +02:00
Fabian Dill
6567f14415 add log_network Server argument 2021-04-07 02:37:21 +02:00
Fabian Dill
32560eac92 send actual NetworkPlayers 2021-04-07 02:20:03 +02:00
Fabian Dill
4c71662719 factorio: award free samples to entire force 2021-04-07 01:55:53 +02:00
Fabian Dill
96a28ed41e implement Factorio option "free_samples" 2021-04-06 21:16:25 +02:00
Fabian Dill
bc1d0ed583 Update Factorio mod to give free samples
(for now always, probably an option later)
2021-04-06 02:20:13 +02:00
Fabian Dill
635897574f clean up technology handling a bit 2021-04-05 15:37:15 +02:00
Fabian Dill
0eca0b2209 add jinja 2 requirement 2021-04-04 11:27:37 +02:00
Fabian Dill
20b72369d8 allow basic WebHost functionality to work 2021-04-04 03:18:19 +02:00
Fabian Dill
d451145d53 Merge branch 'main' into Archipelago_Main 2021-04-04 03:17:46 +02:00
Fabian Dill
4ab59d522d Make bridge notification less spammy 2021-04-04 01:19:54 +02:00
Fabian Dill
250099f5fd Small adjustments 2021-04-03 20:02:15 +02:00
Fabian Dill
c14a150795 Output Factorio mod as zip 2021-04-03 15:06:32 +02:00
Fabian Dill
91bcd59940 implement Factorio options max_science_pack and tech_cost
also give warnings about deprecated LttP options
also fix FactorioClient.py getting stuck if send an unknown item id
also fix !missing having an extra newline after each entry
also default to no webui
2021-04-03 14:47:49 +02:00
Fabian Dill
b871a688a4 correctly add 4 bows to easy item pool
(found by el0)
2021-04-02 14:55:39 +02:00
Fabian Dill
d225eb9ca8 update readme links 2021-04-01 20:46:43 +02:00
69 changed files with 4081 additions and 855 deletions

View File

@@ -24,6 +24,13 @@ class MultiWorld():
plando_connections: List[PlandoConnection]
er_seeds: Dict[int, str]
class AttributeProxy():
def __init__(self, rule):
self.rule = rule
def __getitem__(self, player) -> bool:
return self.rule(player)
def __init__(self, players: int):
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
@@ -56,15 +63,22 @@ class MultiWorld():
self.dynamic_regions = []
self.dynamic_locations = []
self.spoiler = Spoiler(self)
self.fix_trock_doors = self.AttributeProxy(lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_trock_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
self.remote_items = self.AttributeProxy(lambda player: self.game[player] != "A Link to the Past")
for player in range(1, players + 1):
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('tech_tree_layout_prerequisites', {})
set_player_attr('_region_cache', {})
set_player_attr('shuffle', "vanilla")
set_player_attr('logic', "noglitches")
set_player_attr('mode', 'open')
set_player_attr('swords', 'random')
set_player_attr('swordless', False)
set_player_attr('difficulty', 'normal')
set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False)
@@ -74,17 +88,11 @@ class MultiWorld():
set_player_attr('retro', False)
set_player_attr('hints', True)
set_player_attr('player_names', [])
set_player_attr('remote_items', False)
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
set_player_attr('ganon_at_pyramid', True)
set_player_attr('ganonstower_vanilla', True)
set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
set_player_attr('can_access_trock_eyebridge', None)
set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None)
@@ -135,14 +143,12 @@ class MultiWorld():
import Options
for hk_option in Options.hollow_knight_options:
set_player_attr(hk_option, False)
self.worlds = []
#for i in range(players):
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
@property
def NOTCURSED(self): # not here to stay
return {player: not cursed for player, cursed in self.CURSED.items()}
self.custom_data = {}
for player in range(1, players+1):
self.custom_data[player] = {}
# self.worlds = []
# for i in range(players):
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
def secure(self):
self.random = secrets.SystemRandom()
@@ -163,6 +169,11 @@ class MultiWorld():
def factorio_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
@property
def minecraft_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@@ -227,7 +238,8 @@ class MultiWorld():
ret = CollectionState(self)
def soft_collect(item):
if item.name.startswith('Progressive '):
if item.game == "A Link to the Past" and item.name.startswith('Progressive '):
# ALttP items
if 'Sword' in item.name:
if ret.has('Golden Sword', item.player):
pass
@@ -317,7 +329,7 @@ class MultiWorld():
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
item.world = self # try to not have this here anymore
item.world = self # try to not have this here anymore
if collect:
self.state.collect(item, location.event, location)
@@ -399,24 +411,24 @@ class MultiWorld():
if self.has_beaten_game(self.state):
return True
state = CollectionState(self)
prog_locations = {location for location in self.get_locations() if location.item is not None and (
location.item.advancement or location.event) and location not in state.locations_checked}
prog_locations = {location for location in self.get_locations() if location.item
and location.item.advancement and location not in state.locations_checked}
while prog_locations:
sphere = []
sphere = set()
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations:
if location.can_reach(state):
sphere.append(location)
sphere.add(location)
if not sphere:
# ran out of places and did not finish yet, quit
return False
for location in sphere:
prog_locations.remove(location)
state.collect(location.item, True, location)
prog_locations -= sphere
if self.has_beaten_game(state):
return True
@@ -726,7 +738,7 @@ class CollectionState(object):
def can_retrieve_tablet(self, player:int) -> bool:
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
(self.world.swords[player] == "swordless" and
(self.world.swordless[player] and
self.has("Hammer", player)))
def has_sword(self, player: int) -> bool:
@@ -747,7 +759,7 @@ class CollectionState(object):
def can_melt_things(self, player: int) -> bool:
return self.has('Fire Rod', player) or \
(self.has('Bombos', player) and
(self.world.swords[player] == "swordless" or
(self.world.swordless[player] or
self.has_sword(player)))
def can_avoid_lasers(self, player: int) -> bool:
@@ -800,6 +812,91 @@ class CollectionState(object):
rules.append(self.has('Moon Pearl', player))
return all(rules)
# Minecraft logic functions
def has_iron_ingots(self, player: int):
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
def has_gold_ingots(self, player: int):
return self.has('Ingot Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
def has_diamond_pickaxe(self, player: int):
return self.has('Progressive Tools', player, 3) and self.has_iron_ingots(player)
def craft_crossbow(self, player: int):
return self.has('Archery', player) and self.has_iron_ingots(player)
def has_bottle_mc(self, player: int):
return self.has('Bottles', player) and self.has('Ingot Crafting', player)
def can_enchant(self, player: int):
return self.has('Enchanting', player) and self.has_diamond_pickaxe(player) # mine obsidian and lapis
def can_use_anvil(self, player: int):
return self.has('Enchanting', player) and self.has('Resource Blocks', player) and self.has_iron_ingots(player)
def fortress_loot(self, player: int): # saddles, blaze rods, wither skulls
return self.can_reach('Nether Fortress', 'Region', player) and self.basic_combat(player)
def can_brew_potions(self, player: int):
return self.fortress_loot(player) and self.has('Brewing', player) and self.has_bottle_mc(player)
def can_piglin_trade(self, player: int):
return self.has_gold_ingots(player) and (self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region', player))
def enter_stronghold(self, player: int):
return self.fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
# Difficulty-dependent functions
def combat_difficulty(self, player: int):
return self.world.combat_difficulty[player].get_option_name()
def can_adventure(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard':
return True
return self.has('Progressive Weapons', player) and (self.has('Ingot Crafting', player) or self.has('Campfire', player))
def basic_combat(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \
self.has('Shield', player) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard':
return True
return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self.has_iron_ingots(player)
def complete_raid(self, player: int):
reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player)
if self.combat_difficulty(player) == 'easy':
return reach_regions and \
self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \
self.has('Shield', player) and self.has('Archery', player) and \
self.has('Progressive Tools', player, 2) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard': # might be too hard?
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
(self.has('Progressive Armor', player) or self.has('Shield', player))
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
self.has('Progressive Armor', player) and self.has('Shield', player)
def can_kill_wither(self, player: int):
build_wither = self.fortress_loot(player) and (self.can_reach('The Nether', 'Region', player) or self.can_piglin_trade(player))
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
if self.combat_difficulty(player) == 'easy':
return build_wither and normal_kill and self.has('Archery', player)
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
return build_wither and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
return build_wither and normal_kill
def can_kill_ender_dragon(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.has('Archery', player) and \
self.can_brew_potions(player) and self.can_enchant(player)
if self.combat_difficulty(player) == 'hard':
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
if location:
self.locations_checked.add(location)
@@ -870,7 +967,7 @@ class CollectionState(object):
def remove(self, item):
if item.advancement:
to_remove = item.name
if to_remove.startswith('Progressive '):
if item.game == "A Link to the Past" and to_remove.startswith('Progressive '):
if 'Sword' in to_remove:
if self.has('Golden Sword', item.player):
to_remove = 'Golden Sword'
@@ -897,7 +994,7 @@ class CollectionState(object):
elif self.has('Blue Shield', item.player):
to_remove = 'Blue Shield'
else:
to_remove = 'None'
to_remove = None
elif 'Bow' in item.name:
if self.has('Silver Bow', item.player):
to_remove = 'Silver Bow'
@@ -906,7 +1003,7 @@ class CollectionState(object):
else:
to_remove = None
if to_remove is not None:
if to_remove:
self.prog_items[to_remove, item.player] -= 1
if self.prog_items[to_remove, item.player] < 1:
@@ -1111,7 +1208,7 @@ class Location():
@property
def hint_text(self):
return getattr(self, "_hint_text", self.name)
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
class Item():
location: Optional[Location] = None
@@ -1313,7 +1410,7 @@ class Spoiler(object):
'dark_room_logic': self.world.dark_room_logic,
'mode': self.world.mode,
'retro': self.world.retro,
'weapons': self.world.swords,
'swordless': self.world.swordless,
'goal': self.world.goal,
'shuffle': self.world.shuffle,
'item_pool': self.world.difficulty,
@@ -1397,6 +1494,11 @@ class Spoiler(object):
for hk_option in Options.hollow_knight_options:
res = getattr(self.world, hk_option)[player]
outfile.write(f'{hk_option+":":33}{res}\n')
if player in self.world.minecraft_player_ids:
import Options
for mc_option in Options.minecraft_options:
res = getattr(self.world, mc_option)[player]
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
if player in self.world.alttp_player_ids:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
@@ -1412,7 +1514,7 @@ class Spoiler(object):
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %

View File

@@ -129,7 +129,7 @@ class CommonContext():
self.input_requests = 0
# game state
self.player_names: typing.Dict[int: str] = {}
self.player_names: typing.Dict[int: str] = {0: "Server"}
self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event()
@@ -184,7 +184,6 @@ class CommonContext():
async def disconnect(self):
if self.server and not self.server.socket.closed:
await self.server.socket.close()
self.ui_node.send_connection_status(self)
if self.server_task is not None:
await self.server_task
@@ -195,6 +194,7 @@ class CommonContext():
def consume_players_package(self, package: typing.List[tuple]):
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
self.player_names[0] = "Server"
def event_invalid_slot(self):
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
@@ -213,6 +213,14 @@ class CommonContext():
await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address))
def on_print(self, args: dict):
logger.info(args["text"])
def on_print_json(self, args: dict):
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups.
logger.info(self.jsontotextparser(args["data"]))
async def server_loop(ctx: CommonContext, address=None):
ui_node = getattr(ctx, "ui_node", None)
@@ -311,12 +319,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
args['players'].sort()
current_team = -1
logger.info('Players:')
for team, slot, name in args['players']:
if team != current_team:
logger.info(f' Team #{team + 1}')
current_team = team
logger.info(' %s (Player %d)' % (name, slot))
if args["datapackage_version"] > network_data_package["version"]:
for network_player in args['players']:
if network_player.team != current_team:
logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
await ctx.server_auth(args['password'])
@@ -356,7 +364,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if msgs:
await ctx.send_msgs(msgs)
if ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": CLientStatus.CLIENT_GOAL}])
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
# Get the server side view of missing as of time of connecting.
# This list is used to only send to the server what is reported as ACTUALLY Missing.
@@ -394,12 +402,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.hint_points = args['hint_points']
elif cmd == 'Print':
logger.info(args["text"])
ctx.on_print(args)
elif cmd == 'PrintJSON':
if not ctx.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups.
logger.info(ctx.jsontotextparser(args["data"]))
ctx.on_print_json(args)
elif cmd == 'InvalidArguments':
logger.warning(f"Invalid Arguments: {args['text']}")

View File

@@ -2,16 +2,18 @@ import os
import logging
import json
import string
import copy
from concurrent.futures import ThreadPoolExecutor
import colorama
import asyncio
from queue import Queue, Empty
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
from MultiServer import mark_raw
import Utils
import random
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus
from worlds.factorio.Technologies import lookup_id_to_name
@@ -33,10 +35,9 @@ if not os.path.exists(executable):
else:
raise FileNotFoundError(executable)
script_folder = options["factorio_options"]["script-output"]
threadpool = ThreadPoolExecutor(10)
class FactorioCommandProcessor(ClientCommandProcessor):
@mark_raw
def _cmd_factorio(self, text: str) -> bool:
@@ -48,6 +49,12 @@ class FactorioCommandProcessor(ClientCommandProcessor):
return True
return False
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
if not self.ctx.auth:
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
return super(FactorioCommandProcessor, self)._cmd_connect(address)
class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor
@@ -56,13 +63,11 @@ class FactorioContext(CommonContext):
super(FactorioContext, self).__init__(*args, **kwargs)
self.send_index = 0
self.rcon_client = None
self.raw_json_text_parser = RawJSONtoTextParser(self)
async def server_auth(self, password_requested):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
if self.auth is None:
logging.info('Enter the name of your slot to join this game:')
self.auth = await self.console_input()
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils._version_tuple,
@@ -70,34 +75,61 @@ class FactorioContext(CommonContext):
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
}])
def on_print(self, args: dict):
logger.info(args["text"])
if self.rcon_client:
cleaned_text = args['text'].replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
async def game_watcher(ctx: FactorioContext):
research_logger = logging.getLogger("FactorioWatcher")
researches_done_file = os.path.join(script_folder, "research_done.json")
if os.path.exists(researches_done_file):
os.remove(researches_done_file)
from worlds.factorio.Technologies import lookup_id_to_name, tech_table
def on_print_json(self, args: dict):
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups.
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently
logger.info(self.jsontotextparser(args["data"]))
if self.rcon_client:
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
async def game_watcher(ctx: FactorioContext, bridge_file: str):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0
try:
while 1:
if os.path.exists(researches_done_file):
research_logger.info("Found Factorio Bridge file.")
if os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.")
while 1:
with open(researches_done_file) as f:
with open(bridge_file) as f:
data = json.load(f)
research_data = {int(tech_name.split("-")[1]) for tech_name in data if tech_name.startswith("ap-")}
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
ctx.auth = data["slot_name"]
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
research_logger.info(f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
bridge_logger.info(f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1)
else:
research_logger.info("Did not find Factorio Bridge file.")
await asyncio.sleep(5)
bridge_counter += 1
if bridge_counter >= 60:
bridge_logger.info(
"Did not find Factorio Bridge file, "
"waiting for mod to run, which requires the server to run, "
"which requires a player to be connected.")
bridge_counter = 0
await asyncio.sleep(1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
def stream_factorio_output(pipe, queue):
def queuer():
while 1:
@@ -124,6 +156,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue)
stream_factorio_output(factorio_process.stderr, factorio_queue)
script_folder = None
try:
while 1:
while not factorio_queue.empty():
@@ -134,17 +167,28 @@ async def factorio_server_watcher(ctx: FactorioContext):
# trigger lua interface confirmation
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
ctx.rcon_client.send_command("/ap-sync")
if not script_folder and "Write data path:" in msg:
script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json")
if os.path.exists(bridge_file):
os.remove(bridge_file)
logging.info(f"Bridge File Path: {bridge_file}")
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
if ctx.rcon_client:
while ctx.send_index < len(ctx.items_received):
item_id = ctx.items_received[ctx.send_index].item
item_name = lookup_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
response = ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
if response:
factorio_server_logger.info(response)
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player]
if item_id not in lookup_id_to_name:
logging.error(f"Cannot send unknown item ID: {item_id}")
else:
item_name = lookup_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
ctx.send_index += 1
await asyncio.sleep(1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
@@ -158,14 +202,13 @@ async def main():
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
await asyncio.sleep(3)
watcher_task = asyncio.create_task(game_watcher(ctx), name="FactorioProgressionWatcher")
input_task = asyncio.create_task(console_loop(ctx), name="Input")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
await ctx.exit_event.wait()
ctx.server_address = None
ctx.snes_reconnect_address = None
await asyncio.gather(watcher_task, input_task, factorio_server_task)
await asyncio.gather(input_task, factorio_server_task)
if ctx.server is not None and not ctx.server.socket.closed:
await ctx.server.socket.close()

5
Gui.py
View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# module is planned to be removed
from argparse import Namespace
from glob import glob
import json
@@ -1745,7 +1746,8 @@ def update_sprites(task, on_finish=None):
try:
task.update_status("Determining needed sprites")
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
for sprite in sprites_arr if sprite["author"] != "Nintendo"]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
@@ -1909,6 +1911,7 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
return image.zoom(2)
if __name__ == '__main__':
import sys
if "update_sprites" in sys.argv:

View File

@@ -3,7 +3,7 @@ MIT License
Copyright (c) 2017 LLCoolDave
Copyright (c) 2021 Berserker66
Copyright (c) 2021 CaitSith2
Copyright (c) 2020 LegendaryLinux
Copyright (c) 2021 LegendaryLinux
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -5,6 +5,10 @@ import logging
import textwrap
import sys
import time
from tkinter import Tk
from Gui import update_sprites
from GuiUtils import BackgroundTaskProgress
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings
from Utils import output_path
@@ -16,46 +20,67 @@ class AdjusterWorld(object):
self.sprite_pool = {1: sprite_pool}
self.rom_seeds = {1: random}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP rom to adjust.')
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
help='''\
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
help='''\
Hide the triforce hud in certain circumstances.
hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
(Both can be revealed when speaking to Murahalda)
(default: %(default)s)
''')
parser.add_argument('--enableflashing', help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)', action='store_false', dest="reduceflashing")
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
parser.add_argument('--enableflashing',
help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)',
action='store_false', dest="reduceflashing")
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?',
choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
parser.add_argument('--heartcolor', default='red', const='red', nargs='?',
choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--link_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--shield_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--sword_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--hud_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--ow_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--link_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--shield_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--sword_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--hud_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--uw_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
@@ -64,8 +89,11 @@ def main():
sprite that will be extracted.
''')
parser.add_argument('--names', default='', type=str)
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
if args.update_sprites:
run_sprite_update()
sys.exit()
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
@@ -99,13 +127,13 @@ def adjust(args):
else:
raise RuntimeError(
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
palettes_options={}
palettes_options['dungeon']=args.uw_palettes
palettes_options = {}
palettes_options['dungeon'] = args.uw_palettes
palettes_options['overworld']=args.ow_palettes
palettes_options['hud']=args.hud_palettes
palettes_options['sword']=args.sword_palettes
palettes_options['shield']=args.shield_palettes
palettes_options['overworld'] = args.ow_palettes
palettes_options['hud'] = args.hud_palettes
palettes_options['sword'] = args.sword_palettes
palettes_options['shield'] = args.shield_palettes
# palettes_options['link']=args.link_palettesvera
racerom = rom.read_byte(0x180213) > 0
@@ -123,6 +151,7 @@ def adjust(args):
return args, path
def adjustGUI():
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
@@ -148,6 +177,7 @@ def adjustGUI():
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")])
romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
romDialogFrame.pack(side=TOP, expand=True, fill=X)
baseRomLabel2.pack(side=LEFT)
@@ -198,5 +228,16 @@ def adjustGUI():
adjustWindow.mainloop()
def run_sprite_update():
import threading
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
print("Done updating sprites")
if __name__ == '__main__':
main()
main()

View File

@@ -982,8 +982,8 @@ async def main():
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--founditems', default=False, action='store_true',
help='Show items found by other players for themselves.')
parser.add_argument('--disable_web_ui', default=False, action='store_true',
help="Turn off emitting a webserver for the webbrowser based user interface.")
parser.add_argument('--web_ui', default=False, action='store_true',
help="Emit a webserver for the webbrowser based user interface.")
args = parser.parse_args()
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
if args.diff_file:
@@ -1002,7 +1002,7 @@ async def main():
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
port = None
if not args.disable_web_ui:
if args.web_ui:
# Find an available port on the host system to use for hosting the websocket server
while True:
port = randrange(49152, 65535)
@@ -1015,7 +1015,7 @@ async def main():
ctx = Context(args.snes, args.connect, args.password, args.founditems, port)
input_task = asyncio.create_task(console_loop(ctx), name="Input")
if not args.disable_web_ui:
if args.web_ui:
ui_socket = websockets.serve(functools.partial(websocket_server, ctx=ctx),
'localhost', port, ping_timeout=None, ping_interval=None)
await ui_socket

248
Main.py
View File

@@ -10,8 +10,7 @@ import pickle
from typing import Dict
from BaseClasses import MultiWorld, CollectionState, Region, Item
from worlds.alttp import ALttPLocation
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups
from worlds.alttp.Items import ItemFactory, item_name_groups
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
lookup_vanilla_location_to_entrance
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
@@ -23,12 +22,14 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
from worlds.hk import gen_hollow, set_rules as set_hk_rules
from worlds.hk import gen_hollow
from worlds.hk import create_regions as hk_create_regions
from worlds.factorio import gen_factorio, factorio_create_regions
from worlds.factorio.Mod import generate_mod
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
from worlds.minecraft.Regions import minecraft_create_regions
from worlds.generic.Rules import locality_rules
from worlds import Games
from worlds import Games, lookup_any_item_name_to_id
import Patch
seeddigits = 20
@@ -70,7 +71,7 @@ def main(args, seed=None):
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
world.mode = args.mode.copy()
world.swords = args.swords.copy()
world.swordless = args.swordless.copy()
world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
@@ -88,7 +89,6 @@ def main(args, seed=None):
world.hints = args.hints.copy()
world.remote_items = args.remote_items.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
@@ -135,6 +135,10 @@ def main(args, seed=None):
import Options
for hk_option in Options.hollow_knight_options:
setattr(world, hk_option, getattr(args, hk_option, {}))
for factorio_option in Options.factorio_options:
setattr(world, factorio_option, getattr(args, factorio_option, {}))
for minecraft_option in Options.minecraft_options:
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
@@ -166,15 +170,15 @@ def main(args, seed=None):
world.player_names[player].append(name)
logger.info('')
for player in world.player_ids:
for item_name in args.startinventory[player]:
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
world.push_precollected(item)
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids:
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
if item:
world.push_precollected(item)
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
@@ -206,12 +210,15 @@ def main(args, seed=None):
for player in world.factorio_player_ids:
factorio_create_regions(world, player)
for player in world.minecraft_player_ids:
minecraft_create_regions(world, player)
for player in world.alttp_player_ids:
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon)
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
@@ -265,6 +272,9 @@ def main(args, seed=None):
for player in world.factorio_player_ids:
gen_factorio(world, player)
for player in world.minecraft_player_ids:
gen_minecraft(world, player)
logger.info("Running Item Plando")
for item in world.itempool:
@@ -304,9 +314,7 @@ def main(args, seed=None):
balance_multiworld_progression(world)
logger.info('Generating output files.')
outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed)
rom_names = []
def _gen_rom(team: int, player: int):
@@ -416,7 +424,8 @@ def main(args, seed=None):
for player in world.alttp_player_ids:
rom_futures.append(pool.submit(_gen_rom, team, player))
for player in world.factorio_player_ids:
mod_futures.append(pool.submit(generate_mod, world, player))
mod_futures.append(pool.submit(generate_mod, world, player,
str(args.outputname if args.outputname else world.seed)))
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
@@ -479,9 +488,9 @@ def main(args, seed=None):
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
precollected_items = [[] for player in range(world.players)]
precollected_items = {player: [] for player in range(1, world.players+1)}
for item in world.precollected_items:
precollected_items[item.player - 1].append(item.code)
precollected_items[item.player].append(item.code)
FillDisabledShopSlots(world)
@@ -490,7 +499,13 @@ def main(args, seed=None):
for future in roms:
rom_name = future.result()
rom_names.append(rom_name)
minimum_versions = {"server": (0, 0, 2)}
slot_data = {}
client_versions = {}
minimum_versions = {"server": (0, 0, 4), "clients": client_versions}
games = {}
for slot in world.player_ids:
client_versions[slot] = (0, 0, 3)
games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}
@@ -498,24 +513,34 @@ def main(args, seed=None):
for player, name in enumerate(team, 1):
if player not in world.alttp_player_ids:
connect_names[name] = (i, player)
multidata = zlib.compress(pickle.dumps({"names": parsed_names,
"connect_names": connect_names,
"remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player] or
world.game[player] != "A Link to the Past"},
"locations": {
(location.address, location.player):
(location.item.code, location.item.player)
for location in world.get_filled_locations() if
type(location.address) is int},
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"version": tuple(_version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
}), 9)
for slot in world.hk_player_ids:
slots_data = slot_data[slot] = {}
for option_name in Options.hollow_knight_options:
option = getattr(world, option_name)[slot]
slots_data[option_name] = int(option.value)
for slot in world.minecraft_player_ids:
slot_data[slot] = fill_minecraft_slot_data(world, slot)
multidata = zlib.compress(pickle.dumps({
"slot_data" : slot_data,
"games": games,
"names": parsed_names,
"connect_names": connect_names,
"remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player]},
"locations": {
(location.address, location.player):
(location.item.code, location.item.player)
for location in world.get_filled_locations() if
type(location.address) is int},
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"version": tuple(_version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": str(args.outputname if args.outputname else world.seed)
}), 9)
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
f.write(bytes([1])) # version of format
@@ -532,9 +557,11 @@ def main(args, seed=None):
if multidata_task:
multidata_task.result() # retrieve exception if one exists
pool.shutdown() # wait for all queued tasks to complete
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
generate_mc_data(world, player, str(args.outputname if args.outputname else world.seed))
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
create_playthrough(world)
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
@@ -542,155 +569,8 @@ def main(args, seed=None):
return world
def copy_world(world):
# ToDo: Not good yet
# delete now?
ret = MultiWorld(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.item_functionality, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
ret.powder_patch_required = world.powder_patch_required.copy()
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
ret.treasure_hunt_count = world.treasure_hunt_count.copy()
ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
ret.sewer_light_cone = world.sewer_light_cone.copy()
ret.light_world_light_cone = world.light_world_light_cone
ret.dark_world_light_cone = world.dark_world_light_cone
ret.seed = world.seed
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
ret.can_access_trock_front = world.can_access_trock_front.copy()
ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
ret.can_access_trock_middle = world.can_access_trock_middle.copy()
ret.can_take_damage = world.can_take_damage
ret.difficulty_requirements = world.difficulty_requirements.copy()
ret.fix_fake_world = world.fix_fake_world.copy()
ret.mapshuffle = world.mapshuffle.copy()
ret.compassshuffle = world.compassshuffle.copy()
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.open_pyramid = world.open_pyramid.copy()
ret.boss_shuffle = world.boss_shuffle.copy()
ret.enemy_shuffle = world.enemy_shuffle.copy()
ret.enemy_health = world.enemy_health.copy()
ret.enemy_damage = world.enemy_damage.copy()
ret.beemizer = world.beemizer.copy()
ret.timer = world.timer.copy()
ret.shufflepots = world.shufflepots.copy()
ret.shuffle_prizes = world.shuffle_prizes.copy()
ret.shop_shuffle = world.shop_shuffle.copy()
ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
ret.dark_room_logic = world.dark_room_logic.copy()
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
ret.game = world.game.copy()
ret.completion_condition = world.completion_condition.copy()
for player in world.alttp_player_ids:
if world.mode[player] != 'inverted':
create_regions(ret, player)
else:
create_inverted_regions(ret, player)
create_shops(ret, player)
create_dungeons(ret, player)
for player in world.hk_player_ids:
hk_create_regions(ret, player)
copy_dynamic_regions_and_locations(world, ret)
# copy bosses
for dungeon in world.dungeons:
for level, boss in dungeon.bosses.items():
ret.get_dungeon(dungeon.name, dungeon.player).bosses[level] = boss
for shop in world.shops:
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
copied_shop.inventory = copy.copy(shop.inventory)
# connect copied world
for region in world.regions:
copied_region = ret.get_region(region.name, region.player)
copied_region.is_light_world = region.is_light_world
copied_region.is_dark_world = region.is_dark_world
for exit in copied_region.exits:
old_connection = world.get_entrance(exit.name, exit.player).connected_region
exit.connect(ret.get_region(old_connection.name, old_connection.player))
# fill locations
for location in world.get_locations():
if location.item is not None:
item = Item(location.item.name, location.item.advancement, location.item.code, player = location.item.player)
ret.get_location(location.name, location.player).item = item
item.location = ret.get_location(location.name, location.player)
item.world = ret
item.type = location.item.type
item.game = location.item.game
if location.event:
ret.get_location(location.name, location.player).event = True
if location.locked:
ret.get_location(location.name, location.player).locked = True
# copy remaining itempool. No item in itempool should have an assigned location
for old_item in world.itempool:
item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player)
item.type = old_item.type
ret.itempool.append(item)
for old_item in world.precollected_items:
item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player)
item.type = old_item.type
ret.push_precollected(item)
# copy progress items in state
ret.state.prog_items = world.state.prog_items.copy()
ret.state.stale = {player: True for player in range(1, world.players + 1)}
for player in world.alttp_player_ids:
set_rules(ret, player)
for player in world.hk_player_ids:
set_hk_rules(ret, player)
return ret
def copy_dynamic_regions_and_locations(world, ret):
for region in world.dynamic_regions:
new_reg = Region(region.name, region.type, region.hint_text, region.player)
ret.regions.append(new_reg)
ret.initialize_regions([new_reg])
ret.dynamic_regions.append(new_reg)
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
if region.shop:
new_reg.shop = region.shop.__class__(new_reg, region.shop.room_id, region.shop.shopkeeper_config,
region.shop.custom, region.shop.locked, region.shop.sram_offset)
ret.shops.append(new_reg.shop)
for location in world.dynamic_locations:
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player)
new_loc = ALttPLocation(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg)
# todo: this is potentially dangerous. later refactor so we
# can apply dynamic region rules on top of copied world like other rules
new_loc.access_rule = location.access_rule
new_loc.always_allow = location.always_allow
new_loc.item_rule = location.item_rule
new_reg.locations.append(new_loc)
ret.clear_location_cache()
def create_playthrough(world):
"""Destructive to the world it is run on."""
"""Destructive to the world while it is run, damage gets repaired afterwards."""
# get locations containing progress items
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
state_cache = [None]

View File

@@ -5,6 +5,7 @@ import threading
import concurrent.futures
import argparse
import logging
import random
def feedback(text: str):
@@ -24,7 +25,7 @@ if __name__ == "__main__":
args = parser.parse_args()
from Utils import get_public_ipv4, get_options
from Mystery import get_seed_name
from Patch import create_patch_file
options = get_options()
@@ -89,10 +90,11 @@ if __name__ == "__main__":
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
else:
logging.info(f"{target_player_count} Players found.")
seed_name = get_seed_name(random)
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\""
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\" " \
f"--seed_name {seed_name}"
if create_spoiler:
command += " --create_spoiler"
@@ -117,15 +119,9 @@ if __name__ == "__main__":
start = time.perf_counter()
text = subprocess.check_output(command, shell=True).decode()
logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
seedname = ""
for segment in text.split():
if segment.startswith("M"):
seedname = segment
break
multidataname = f"AP_{seedname}.archipelago"
spoilername = f"AP_{seedname}_Spoiler.txt"
multidataname = f"AP_{seed_name}.archipelago"
spoilername = f"AP_{seed_name}_Spoiler.txt"
romfilename = ""
if player_name:
@@ -162,7 +158,7 @@ if __name__ == "__main__":
logging.info(f"Removed {file} which is now present in the zipfile")
zipname = os.path.join(output_path, f"AP_{seedname}.{typical_zip_ending}")
zipname = os.path.join(output_path, f"AP_{seed_name}.{typical_zip_ending}")
logging.info(f"Creating zipfile {zipname}")
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
@@ -186,7 +182,7 @@ if __name__ == "__main__":
futures = []
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
for file in os.listdir(output_path):
if seedname in file:
if seed_name in file:
if file.endswith(".sfc"):
futures.append(pool.submit(_handle_sfc_file, file))
elif file.endswith(".apbp"):

View File

@@ -29,9 +29,9 @@ from worlds.alttp import Items, Regions
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_item_name_to_id, \
lookup_any_location_id_to_name, lookup_any_location_name_to_id
import Utils
from Utils import get_item_name_from_id, get_location_name_from_address, \
from Utils import get_item_name_from_id, get_location_name_from_id, \
_version_tuple, restricted_loads, Version
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
colorama.init()
lttp_console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
@@ -110,7 +110,9 @@ class Context(Node):
self.auto_saver_thread = None
self.save_dirty = False
self.tags = ['AP']
self.minimum_client_versions: typing.Dict[typing.Tuple[int, int], Utils.Version] = {}
self.games = {}
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
self.seed_name = ""
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
with open(multidatapath, 'rb') as f:
@@ -119,37 +121,48 @@ class Context(Node):
self._load(self._decompress(data), use_embedded_server_options)
self.data_filename = multidatapath
def _decompress(self, data: bytes) -> dict:
@staticmethod
def _decompress(data: bytes) -> dict:
format_version = data[0]
if format_version != 1:
raise Exception("Incompatible multidata.")
return restricted_loads(zlib.decompress(data[1:]))
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > Utils._version_tuple:
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
f"however this server is of version {Utils._version_tuple}")
clients_ver = decoded_obj["minimum_versions"].get("clients", [])
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
self.minimum_client_versions = {}
for team, player, version in clients_ver:
self.minimum_client_versions[team, player] = Utils.Version(*version)
for player, version in clients_ver.items():
self.minimum_client_versions[player] = Utils.Version(*version)
for team, names in enumerate(decoded_obj['names']):
for player, name in enumerate(names, 1):
self.player_names[(team, player)] = name
self.seed_name = decoded_obj["seed_name"]
self.connect_names = decoded_obj['connect_names']
self.remote_items = decoded_obj['remote_items']
self.locations = decoded_obj['locations']
self.slot_data = decoded_obj['slot_data']
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
for player, loc_data in decoded_obj["er_hint_data"].items()}
self.games = decoded_obj["games"]
# award remote-items start inventory:
for team in range(len(decoded_obj['names'])):
for slot, item_codes in decoded_obj["precollected_items"].items():
if slot in self.remote_items:
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
if use_embedded_server_options:
server_options = decoded_obj.get("server_options", {})
self._set_options(server_options)
def get_players_package(self):
return [(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
def _set_options(self, server_options: dict):
for key, value in server_options.items():
@@ -331,11 +344,14 @@ async def server(websocket, path, ctx: Context):
ctx.endpoints.append(client)
try:
logging.info("Incoming")
if ctx.log_network:
logging.info("Incoming connection")
await on_client_connected(ctx, client)
logging.info("Sent Room Info")
if ctx.log_network:
logging.info("Sent Room Info")
async for data in websocket:
logging.info(data)
if ctx.log_network:
logging.info(f"Incoming message: {data}")
for msg in decode(data):
await process_client_cmd(ctx, client, msg)
except Exception as e:
@@ -350,7 +366,7 @@ async def on_client_connected(ctx: Context, client: Client):
await ctx.send_msgs(client, [{
'cmd': 'RoomInfo',
'password': ctx.password is not None,
'players': [(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name)) for client
'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client
in ctx.endpoints if client.auth],
# tags are for additional features in the communication.
# Name them by feature or fork, as you feel is appropriate.
@@ -360,7 +376,8 @@ async def on_client_connected(ctx: Context, client: Client):
'remaining_mode': ctx.remaining_mode,
'hint_cost': ctx.hint_cost,
'location_check_points': ctx.location_check_points,
'datapackage_version': network_data_package["version"]
'datapackage_version': network_data_package["version"],
'seed_name': ctx.seed_name
}])
@@ -463,8 +480,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
get_received_items(ctx, team, target_player).append(new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
@@ -515,7 +532,7 @@ def collect_hints_location(ctx: Context, team: int, slot: int, location: str) ->
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
f"{lookup_any_item_id_to_name[hint.item]} is " \
f"at {get_location_name_from_address(hint.location)} " \
f"at {get_location_name_from_id(hint.location)} " \
f"in {ctx.player_names[team, hint.finding_player]}'s World"
if hint.entrance:
@@ -525,10 +542,15 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
parts = []
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
NetUtils.add_json_text(parts, " sent ")
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
NetUtils.add_json_text(parts, " to ")
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
if net_item.player == receiving_player:
NetUtils.add_json_text(parts, " found their ")
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
else:
NetUtils.add_json_text(parts, " sent ")
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
NetUtils.add_json_text(parts, " to ")
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
NetUtils.add_json_text(parts, " (")
NetUtils.add_json_text(parts, net_item.location, type=NetUtils.JSONTypes.location_id)
NetUtils.add_json_text(parts, ")")
@@ -794,7 +816,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client)
if locations:
texts = [f'Missing: {get_item_name_from_id(location)}\n' for location in locations]
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:
@@ -967,6 +989,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
errors.add('InvalidSlot')
else:
team, slot = ctx.connect_names[args['name']]
game = ctx.games[slot]
if args['game'] != game:
errors.add('InvalidSlot')
# this can only ever be 0 or 1 elements
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
if clients:
@@ -982,14 +1007,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.name = ctx.player_names[(team, slot)]
client.team = team
client.slot = slot
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
minver = ctx.minimum_client_versions[slot]
if minver > args['version']:
errors.add('IncompatibleVersion')
if ctx.compatibility == 1 and "AP" not in args['tags']:
errors.add('IncompatibleVersion')
#only exact version match allowed
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
# only exact version match allowed
if ctx.compatibility == 0 and args['version'] != _version_tuple:
errors.add('IncompatibleVersion')
if errors:
logging.info(f"A client connection was refused due to: {errors}")
@@ -1004,7 +1027,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"team": client.team, "slot": client.slot,
"players": ctx.get_players_package(),
"missing_locations": get_missing_checks(ctx, client),
"checked_locations": get_checked_checks(ctx, client)}]
"checked_locations": get_checked_checks(ctx, client),
"slot_data": ctx.slot_data.get(client.slot, {})
}]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
@@ -1030,20 +1055,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == 'LocationScouts':
locs = []
for location in args["locations"]:
if type(location) is not int or 0 >= location > len(Regions.location_table):
if type(location) is not int or location not in lookup_any_location_id_to_name:
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
return
loc_name = list(Regions.location_table.keys())[location - 1]
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
target_item, target_player = ctx.locations[location, client.slot]
locs.append(NetworkItem(target_item, location, target_player))
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
item_type = [i[1] for i in Items.item_table.values() if type(i[2]) is int and i[2] == target_item]
if item_type:
target_item = replacements.get(item_type[0], target_item)
locs.append([target_item, location, target_player])
# logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
elif cmd == 'StatusUpdate':
@@ -1309,6 +1326,7 @@ def parse_args() -> argparse.Namespace:
#1 -> recommended for friendly racing, tries to block third party clients
#0 -> recommended for tournaments to force a level playing field, only allow an exact version match
""")
parser.add_argument('--log_network', default=defaults["log_network"], action="store_true")
args = parser.parse_args()
return args
@@ -1345,7 +1363,7 @@ async def main(args: argparse.Namespace):
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
args.auto_shutdown, args.compatibility)
ctx.log_network = args.log_network
data_filename = args.multidata
try:

View File

@@ -54,13 +54,15 @@ def mystery_argparse():
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default="bosses",
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument('--seed_name')
for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args()
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args
def get_seed_name(random):
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None, callback=ERmain):
if not args:
@@ -68,9 +70,8 @@ def main(args=None, callback=ERmain):
seed = get_seed(args.seed)
random.seed(seed)
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
seed_name = args.seed_name if args.seed_name else get_seed_name(random)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
if args.race:
random.seed() # reset to time-based random source
@@ -112,7 +113,7 @@ def main(args=None, callback=ERmain):
erargs.glitch_triforce = args.glitch_triforce
erargs.race = args.race
erargs.skip_playthrough = args.skip_playthrough
erargs.outputname = seedname
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
erargs.teams = args.teams
@@ -195,14 +196,18 @@ def main(args=None, callback=ERmain):
pre_rolled = dict()
pre_rolled["original_seed_number"] = seed
pre_rolled["original_seed_name"] = seedname
pre_rolled["original_seed_name"] = seed_name
pre_rolled["pre_rolled"] = vars(settings).copy()
if "plando_items" in pre_rolled["pre_rolled"]:
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in pre_rolled["pre_rolled"]["plando_items"]]
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in
pre_rolled["pre_rolled"]["plando_items"]]
if "plando_connections" in pre_rolled["pre_rolled"]:
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in pre_rolled["pre_rolled"]["plando_connections"]]
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in
pre_rolled["pre_rolled"][
"plando_connections"]]
with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
with open(os.path.join(args.outputpath if args.outputpath else ".",
f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
yaml.dump(pre_rolled, f)
for k, v in vars(settings).items():
if v is not None:
@@ -294,7 +299,8 @@ def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name] += 1
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
NUMBER=(name_counter[name] if name_counter[name] > 1 else ''),
NUMBER=(name_counter[name] if name_counter[
name] > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
return new_name.strip().replace(' ', '_')[:16]
@@ -315,17 +321,44 @@ available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if leve
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'normal': 'normal',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
# remove sometime before 1.0.0, warn before
legacy_boss_shuffle_options = {
# legacy, will go away:
'simple': 'basic',
'random': 'full',
'normal': 'full'
}
legacy_goals = {
'dungeons': 'bosses',
'fast_ganon': 'crystals',
}
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}')
new_options = set(new_weights) - set(weights)
@@ -337,6 +370,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
f'This is probably in error.')
return weights
def roll_linked_options(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]:
@@ -349,7 +383,8 @@ def roll_linked_options(weights: dict) -> dict:
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", option_set["name"])
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
option_set["name"])
weights["rom"] = rom_weights
else:
logging.debug(f"linked option {option_set['name']} skipped.")
@@ -358,10 +393,11 @@ def roll_linked_options(weights: dict) -> dict:
f"Please fix your linked option.") from e
return weights
def roll_triggers(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
for option_set in weights["triggers"]:
for i, option_set in enumerate(weights["triggers"]):
try:
key = get_choice("option_name", option_set)
if key not in weights:
@@ -373,18 +409,25 @@ def roll_triggers(weights: dict) -> dict:
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
if "options" in option_set:
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom", option_set["option_name"])
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
option_set["option_name"])
weights["rom"] = rom_weights
weights[key] = result
except Exception as e:
raise ValueError(f"A trigger is destroyed. "
raise ValueError(f"Your trigger number {i+1} is destroyed. "
f"Please fix your triggers.") from e
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in legacy_boss_shuffle_options:
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
f"please use {new_boss_shuffle} instead")
return new_boss_shuffle
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
@@ -392,6 +435,10 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in legacy_boss_shuffle_options:
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss} is deprecated, "
f"please use {remainder_shuffle} instead")
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
@@ -419,7 +466,7 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses", ))):
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
if "pre_rolled" in weights:
pre_rolled = weights["pre_rolled"]
@@ -435,7 +482,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
if "plando_connections" in pre_rolled:
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
connection["exit"],
connection["direction"]) for connection in pre_rolled["plando_connections"]]
connection["direction"]) for connection in
pre_rolled["plando_connections"]]
if "connections" not in plando_options and pre_rolled["plando_connections"]:
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
@@ -480,17 +528,42 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights)
if isinstance(itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
ret.startinventory = startitems
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, weights, plando_options)
elif ret.game == "Hollow Knight":
for option_name, option in Options.hollow_knight_options.items():
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
elif ret.game == "Factorio":
pass
for option_name, option in Options.factorio_options.items():
if option_name in weights:
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
setattr(ret, option_name, option.from_any(weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else:
setattr(ret, option_name, option(option.default))
elif ret.game == "Minecraft":
for option_name, option in Options.minecraft_options.items():
if option_name in weights:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else:
setattr(ret, option_name, option(option.default))
else:
raise Exception(f"Unsupported game {ret.game}")
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
@@ -533,17 +606,11 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights, 'ganon')
ret.goal = {'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt'
}[goal]
if goal in legacy_goals:
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
goal = legacy_goals[goal]
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
@@ -587,11 +654,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.hints = get_choice('hints', weights)
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[get_choice('weapons', weights, 'assured')]
ret.swordless = get_choice('swordless', weights, False)
ret.difficulty = get_choice('item_pool', weights)
@@ -602,6 +665,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
@@ -647,23 +711,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights)
if item.startswith(('Progressive ', 'Small Key ', 'Rupee', 'Piece of Heart', 'Boss Heart Container',
'Sanctuary Heart Container', 'Arrow', 'Bombs ', 'Bomb ', 'Bottle')) and isinstance(
itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
ret.startinventory = ','.join(startitems)
ret.glitch_boots = get_choice('glitch_boots', weights, True)
ret.remote_items = get_choice('remote_items', weights, False)
if get_choice("local_keys", weights, "l" in dungeon_items):
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
@@ -772,5 +821,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.quickswap = True
ret.sprite = "Link"
if __name__ == '__main__':
main()

View File

@@ -9,6 +9,7 @@ import websockets
from Utils import Version
class JSONMessagePart(typing.TypedDict, total=False):
text: str
# optional
@@ -18,7 +19,6 @@ class JSONMessagePart(typing.TypedDict, total=False):
found: bool
class ClientStatus(enum.IntEnum):
CLIENT_UNKNOWN = 0
CLIENT_CONNECTED = 5
@@ -61,10 +61,12 @@ _encode = JSONEncoder(
def encode(obj):
return _encode(_scan_for_TypedTuples(obj))
def get_any_version(data: dict) -> Version:
data = {key.lower(): value for key, value in data.items()} # .NET version classes have capitalized keys
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
whitelist = {"NetworkPlayer": NetworkPlayer,
"NetworkItem": NetworkItem,
}
@@ -73,6 +75,7 @@ custom_hooks = {
"Version": get_any_version
}
def _object_hook(o: typing.Any) -> typing.Any:
if isinstance(o, dict):
hook = custom_hooks.get(o.get("class", None), None)
@@ -82,7 +85,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
if cls:
for key in tuple(o):
if key not in cls._fields:
del(o[key])
del (o[key])
return cls(**o)
return o
@@ -99,6 +102,7 @@ class Node:
def __init__(self):
self.endpoints = []
super(Node, self).__init__()
self.log_network = 0
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
@@ -114,6 +118,9 @@ class Node:
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
@@ -123,6 +130,9 @@ class Node:
except websockets.ConnectionClosed:
logging.exception("Exception during send_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
@@ -144,11 +154,16 @@ class HandlerMeta(type):
handlers = attrs["handlers"] = {}
trigger: str = "_handle_"
for base in bases:
handlers.update(base.commands)
handlers.update(base.handlers)
handlers.update({handler_name[len(trigger):]: method for handler_name, method in attrs.items() if
handler_name.startswith(trigger)})
orig_init = attrs.get('__init__', None)
if not orig_init:
for base in bases:
orig_init = getattr(base, '__init__', None)
if orig_init:
break
def __init__(self, *args, **kwargs):
# turn functions into bound methods
@@ -160,6 +175,7 @@ class HandlerMeta(type):
attrs['__init__'] = __init__
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
class JSONTypes(str, enum.Enum):
color = "color"
text = "text"
@@ -171,6 +187,7 @@ class JSONTypes(str, enum.Enum):
location_id = "location_id"
entrance_name = "entrance_name"
class JSONtoTextParser(metaclass=HandlerMeta):
def __init__(self, ctx):
self.ctx = ctx
@@ -229,6 +246,11 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
class RawJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
return self._handle_text(node)
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
@@ -274,7 +296,7 @@ class Hint(typing.NamedTuple):
add_json_text(parts, " is at ")
add_json_text(parts, self.location, type="location_id")
add_json_text(parts, " in ")
add_json_text(parts, self.finding_player, type ="player_id")
add_json_text(parts, self.finding_player, type="player_id")
if self.entrance:
add_json_text(parts, "'s World at ")
add_json_text(parts, self.entrance, type="entrance_name")
@@ -285,4 +307,4 @@ class Hint(typing.NamedTuple):
else:
add_json_text(parts, ".")
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}

View File

@@ -3,7 +3,7 @@ import typing
class AssembleOptions(type):
def __new__(cls, name, bases, attrs):
def __new__(mcs, name, bases, attrs):
options = attrs["options"] = {}
name_lookup = attrs["name_lookup"] = {}
for base in bases:
@@ -17,12 +17,13 @@ class AssembleOptions(type):
# apply aliases, without name_lookup
options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")})
return super(AssembleOptions, cls).__new__(cls, name, bases, attrs)
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
class Option(metaclass=AssembleOptions):
value: int
name_lookup: typing.Dict[int, str]
default = 0
def __repr__(self):
return f"{self.__class__.__name__}({self.get_option_name()})"
@@ -47,6 +48,7 @@ class Option(metaclass=AssembleOptions):
class Toggle(Option):
option_false = 0
option_true = 1
default = 0
def __init__(self, value: int):
self.value = value
@@ -86,6 +88,7 @@ class Toggle(Option):
def get_option_name(self):
return bool(self.value)
class Choice(Option):
def __init__(self, value: int):
self.value: int = value
@@ -100,8 +103,41 @@ class Choice(Option):
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
@classmethod
def from_any(cls, data: typing.Any):
return cls.from_text(data)
def from_any(cls, data: typing.Any) -> Choice:
if type(data) == int and data in cls.options.values():
return cls(data)
return cls.from_text(str(data))
class OptionNameSet(Option):
default = frozenset()
def __init__(self, value: typing.Set[str]):
self.value: typing.Set[str] = value
@classmethod
def from_text(cls, text: str) -> OptionNameSet:
return cls({option.strip() for option in text.split(",")})
@classmethod
def from_any(cls, data: typing.Any) -> OptionNameSet:
if type(data) == set:
return cls(data)
return cls.from_text(str(data))
class OptionDict(Option):
default = {}
def __init__(self, value: typing.Dict[str, typing.Any]):
self.value: typing.Dict[str, typing.Any] = value
@classmethod
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
if type(data) == dict:
return cls(data)
else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
class Logic(Choice):
@@ -231,7 +267,94 @@ hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
"SHADESKIPS": Toggle,
}
hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options, **hollow_knight_skip_options}
hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options,
**hollow_knight_skip_options}
class MaxSciencePack(Choice):
option_automation_science_pack = 0
option_logistic_science_pack = 1
option_military_science_pack = 2
option_chemical_science_pack = 3
option_production_science_pack = 4
option_utility_science_pack = 5
option_space_science_pack = 6
default = 6
def get_allowed_packs(self):
return {option.replace("_", "-") for option, value in self.options.items()
if value <= self.value}
class TechCost(Choice):
option_very_easy = 0
option_easy = 1
option_kind = 2
option_normal = 3
option_hard = 4
option_very_hard = 5
option_insane = 6
default = 3
class FreeSamples(Choice):
option_none = 0
option_single_craft = 1
option_half_stack = 2
option_stack = 3
default = 3
class TechTreeLayout(Choice):
option_single = 0
option_small_diamonds = 1
option_medium_diamonds = 2
option_pyramid = 3
option_funnel = 4
default = 0
class Visibility(Choice):
option_none = 0
option_sending = 1
default = 1
class FactorioStartItems(OptionDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19}
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
"tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost,
"free_samples": FreeSamples,
"visibility": Visibility,
"random_tech_ingredients": Toggle,
"starting_items": FactorioStartItems}
class AdvancementGoal(Choice):
option_few = 0
option_normal = 1
option_many = 2
default = 1
class CombatDifficulty(Choice):
option_easy = 0
option_normal = 1
option_hard = 2
default = 1
minecraft_options: typing.Dict[str, type(Option)] = {
"advancement_goal": AdvancementGoal,
"combat_difficulty": CombatDifficulty,
"include_hard_advancements": Toggle,
"include_insane_advancements": Toggle,
"include_postgame_advancements": Toggle,
"shuffle_structures": Toggle
}
if __name__ == "__main__":
import argparse

View File

@@ -1,12 +1,14 @@
# [Archipelago](https://archipelago.gg) ![Discord Shield](https://discordapp.com/api/guilds/731205301247803413/widget.png?style=shield) | [Install](https://github.com/Berserker66/MultiWorld-Utilities/releases)
# [Archipelago](https://archipelago.gg) ![Discord Shield](https://discordapp.com/api/guilds/731205301247803413/widget.png?style=shield) | [Install](https://github.com/ArchipelagoMW/Archipelago/releases)
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself.
Currently, the following games are supported:
* The Legend of Zelda: A Link to the Past
* Factorio
* Minecraft
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
Downloads can be found at [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases), including compiled
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries.
## History
@@ -25,7 +27,7 @@ We recognize that there is a strong community of incredibly smart people that ha
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page then download and run the appropriate installer. The installers function on Windows only.
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).

View File

@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
minor: int
build: int
__version__ = "0.0.2"
__version__ = "0.1.0"
_version_tuple = tuplize_version(__version__)
import builtins
@@ -84,9 +84,13 @@ def local_path(*path):
# cx_Freeze
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
# we are running in a normal Python environment
import __main__
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
if hasattr(__main__, "__file__"):
# we are running in a normal Python environment
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
else:
# pray
local_path.cached_path = os.path.abspath(".")
return os.path.join(local_path.cached_path, *path)
@@ -166,7 +170,6 @@ def get_default_options() -> dict:
},
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
"script-output": "factorio\\script-output",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
@@ -190,6 +193,7 @@ def get_default_options() -> dict:
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"multi_mystery_options": {
"teams": 1,
@@ -267,14 +271,14 @@ def get_options() -> dict:
return get_options.options
def get_item_name_from_id(code):
def get_item_name_from_id(code: int) -> str:
from worlds import lookup_any_item_id_to_name
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
def get_location_name_from_address(address):
def get_location_name_from_id(code: int) -> str:
from worlds import lookup_any_location_id_to_name
return lookup_any_location_id_to_name.get(address, f'Unknown location (ID:{address})')
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
def persistent_store(category: str, key: typing.Any, value: typing.Any):
@@ -385,7 +389,7 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus"}:
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
import NetUtils
return getattr(NetUtils, name)
# Forbid everything else.

View File

@@ -46,6 +46,7 @@ app.config["PONY"] = {
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "simple"
app.config["JSON_AS_ASCII"] = False
app.config["PATCH_TARGET"] = "archipelago.gg"
app.autoversion = True

View File

@@ -9,13 +9,13 @@ import socket
import threading
import time
import random
import zlib
import pickle
from .models import *
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, parse_yaml
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
class CustomClientMessageProcessor(ClientMessageProcessor):
@@ -27,7 +27,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
self.ctx.save()
self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}")
return True
elif platform.lower().startswith("y"): # youtube
elif platform.lower().startswith("y"): # youtube
self.ctx.video[self.client.team, self.client.slot] = "Youtube", user
self.ctx.save()
self.output(f"Registered Youtube Stream for {user}")
@@ -81,16 +81,16 @@ class WebHostContext(Context):
def init_save(self, enabled: bool = True):
self.saving = enabled
if self.saving:
existing_savegame = Room.get(id=self.room_id).multisave
if existing_savegame:
self.set_save(existing_savegame)
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
self._start_async_saving()
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@db_session
def _save(self, exit_save:bool = False) -> bool:
room = Room.get(id=self.room_id)
room.multisave = self.get_save()
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = datetime.utcnow()

View File

@@ -16,7 +16,7 @@ def download_patch(room_id, patch_id):
room = Room.get(id=room_id)
last_port = room.last_port
patch_data = update_patch_data(patch.data, server=f"{app.config['HOSTNAME']}:{last_port}")
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
patch_data = io.BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"

View File

@@ -0,0 +1,137 @@
# Minecraft Randomizer Setup Guide
## Required Software
### Server Host
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
### Players
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Installation Procedures
### Dedicated Server Setup
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
- This will create the appropriate directories for you to place the files in the following step.
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!
### Basic Player Setup
- Purchase and install Minecraft from the above link.
**You're Done**.
Players only need to have a Vanilla unmodified version of Minecraft to play!
### Advanced Player Setup
***This is not required to play a randomized minecraft game.***
however this recommended as it helps make the experience more enjoyable.
#### Recomended Mods
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
1. Install and run Minecraft from the link above at least once.
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install client**.
- Start Minecraft forge at least once to create the directories needed for the next steps.
3. Navigate to your minecraft install directory and place desired mods `.jar` file the in the `mods` directory.
- The default install directories are as follows.
- Windows `%APPDATA%\.minecraft\mods`
- macOS `~/Library/Application Support/minecraft/mods`
- Linux `~/.minecraft/mods`
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
each player to enjoy an experience customized for their taste, and different players in the same multiworld
can all have different options.
### Where do I get a YAML file?
A basic minecraft yaml will look like this.
```yaml
description: Template Name
# Your name in-game. Spaces will be replaced with underscores and
# there is a 16 character limit
name: YourName
game: Minecraft
# Shared Options supported by all games:
accessibility: locations
progression_balancing: off
# Minecraft Specific Options
# Number of advancements required (out of 92 total) to spawn the
# Ender Dragon and complete the game.
advancement_goal:
few: 0 #30
normal: 1 #50
many: 0 #70
# Modifies the level of items logically required for exploring
# dangerous areas and fighting bosses.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
include_hard_advancements:
on: 0
off: 1
# Junk-fills extremely difficult advancements;
# this is only How Did We Get Here? and Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Some advancements require defeating the Ender Dragon;
# this will junk-fill them so you won't have to finish to send some items.
include_postgame_advancements:
on: 0
off: 1
#enables shuffling of villages, outposts, fortresses, bastions, and end cities.
shuffle_structures:
on: 1
off: 0
```
For more detail on what each setting does check the default `PlayerSettings.yaml` that comes with the Archipelago install.
## Joining a MultiWorld Game
### Obtain your Minecraft data file
**Only one yaml file needs to be submitted per minecraft world regardless of how many players play on it.**
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
is done, the host will provide you with either a link to download your data file, or with a zip file containing
everyone's data files. Your data file should have a `.apmc` extension.
Put your data file in your forge servers `APData` folder. Make sure to remove any previous data file that was in there
previously.
### Connect to the MultiServer
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
Once in game type `/connect <AP-Address> (<Password>)` where `<AP-Address>` is the address of the
Archipelago server. `(<Password>)`
is only required if the Archipleago server you are using has a password set.
### Play the game
When the console tells you that you have joined the room, you're ready to begin playing. Congratulations
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
forge server.

View File

@@ -0,0 +1,121 @@
# Guia instalación de Minecraft Randomizer
## Software Requerido
### Servidor
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
### Jugadores
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Procedimiento de instalación
### Instalación de servidor dedicado
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
1. Descarga el instalador de **Minecraft Forge** 1.16.15 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
- Esto creara la estructura de directorios apropiada para el siguiente paso
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
### Instalación basica para jugadores
- Compra e instala Minecraft a traves del tercer enlace.
**Y listo!**.
Los jugadores solo necesitan una version no modificada de Minecraft para jugar!
### Instalación avanzada para jugadores
***Esto no es requerido para jugar a minecraft randomizado.***
Sin embargo lo recomendamos porque hace la experiencia mas llevadera.
#### Recomended Mods
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
1. Instala y ejecuta Minecraft al menos una vez.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elige **install client**.
- Ejecuta Minecraft forge al menos una vez para generar los directorios necesarios para el siguiente paso.
3. Navega a la carpeta de instalación de Minecraft y colocal los mods que quieras en el directorio `mods`
- Los directorios por defecto de instalación son:
- Windows `%APPDATA%\.minecraft\mods`
- macOS `~/Library/Application Support/minecraft/mods`
- Linux `~/.minecraft/mods`
## Configura tu fichero YAML
### Que es un fichero YAML y potque necesito uno?
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu juego.
Cada jugador de un multiworld entregara u propio fichero YAML.
Esto permite que cada jugador disfrute de una experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld
pueden tener diferentes opciones
### Where do I get a YAML file?
Un fichero basico yaml para minecraft tendra este aspecto.
```yaml
# Usado para describir tu yaml. Util si tienes multiples ficheros
description: Template Name
# Tu nombre en el juego. Los espacios son reemplazados por guiones bajos, limitado a 16 caracteres
name: YourName
game: Minecraft
accessibility: locations
# Recomendado no activar esto ya que el pool de objetos de Minecraft es bastante escueto, ademas hay muchas maneras alternativas de obtener los objetivos de Minecraft.
progression_balancing: off
# Cuantos avances se necesitan para hacer aparecer el Ender Dragon y acabar el juego. few = 30, normal = 50 , many = 70
advancement_goal:
few: 0
normal: 1
many: 0
# Modifica el nivel de objetos lógicamente requeridos para explorar areas peligrosas y pelear contra jefes.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Avances que sean tediosos o basados en suerte tendran simplemente experiencia o cosas no necesarias
include_hard_advancements:
on: 0
off: 1
# Los avances extremadamente difíciles no seran requeridos; esto afecta a How Did We Get Here? y Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Los avances posteriores a Ender Dragon no tendrán objetos necesarios para que otros jugadores en el caso de un MW acaben su partida.
include_postgame_advancements:
on: 0
off: 1
# Actualmente desactivado; permite la mezcla de pueblos, puestos, fortalezas, bastiones y cuidades.
shuffle_structures:
on: 0
off: 1
```
## Unirse a un juego MultiWorld
### Obten tu ficheros de datos Minecraft
**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.**
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld (no confundir con hospedar el mundo minecraft).
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
Tu fichero de datos tiene una extensión `.apmc`.
Pon tu fichero de datos en el directorio `APData` de tu forge server. Asegurate de eliminar los que hubiera anteriormente
### Conectar al multiserver
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
Una vez en juego introduce `/connect <AP-Address> (<Password>)` donde `<AP-Address>` es la dirección del servidor
Archipelago. `(<Password>)`
solo se necesita si el servidor Archipleago tiene un password activo.
### Jugar al juego
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades
por unirte exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor forge.

View File

@@ -0,0 +1,114 @@
# Minecraft Randomizer Uppsättningsguide
## Nödvändig Mjukvara
### Server Värd
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
### Spelare
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Installationsprocedurer
### Tillägnad
Bara en person behöver göra denna uppsättning och vara värd för en server för alla andra spelare att koppla till.
1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste rekommenderade versionen.
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera server**.
- På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är viktigt för nästa steg.
3. Navigera till vart du har installerat servern och öppna `forge-1.16.5-xx.x.x-installer.jar`
- Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta till `eula=true` för att acceptera Minecrafts EULA.
- Detta kommer skapa de lämpliga katalogerna för dig att placera filerna i de följande steget.
4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge server.
- Kör servern igen. Den kommer ladda up och generera den nödvändiga katalogen `APData` för när du är redo att spela!
### Grundläggande Spelaruppsättning
- Köp och installera Minecraft från länken ovanför.
**Du är klar**.
Andra spelare behöver endast ha en 'Vanilla' omodifierad version av Minecraft för att kunna spela!
### Avancerad Spelaruppsättning
***Detta är inte nödvändigt för att spela ett slumpmässigt Minecraftspel.***
Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer trevligt.
#### Rekommenderade Moddar
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
1. Installera och Kör Minecraft från länken ovanför minst en gång.
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera klient**.
- Starta Minecraft Forge minst en gång för att skapa katalogerna som behövs för de nästa stegen.
3. Navigera till din Minecraft installationskatalog och placera de önskade moddarna med `.jar` i `mods` -katalogen.
- Standardinstallationskatalogerna är som följande;
- Windows `%APPDATA%\.minecraft\mods`
- macOS `~/Library/Application Support/minecraft/mods`
- Linux `~/.minecraft/mods`
## Konfigurera Din YAML-fil
### Vad är en YAML-fil och varför behöver jag en?
Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur
den borde generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter
varje spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld
kan ha helt olika alternativ.
### Vart kan jag få tag i en YAML-fil?
En grundläggande Minecraft YAML kommer se ut så här.
```yaml
description: Template Name
# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns.
name: YourName
game: Minecraft
accessibility: locations
progression_balancing: off
advancement_goal:
few: 0
normal: 1
many: 0
combat_difficulty:
easy: 0
normal: 1
hard: 0
include_hard_advancements:
on: 0
off: 1
include_insane_advancements:
on: 0
off: 1
include_postgame_advancements:
on: 0
off: 1
shuffle_structures:
on: 1
off: 0
```
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med Archipelago-installationen.
## Gå med i ett Multivärld-spel
### Skaffa din Minecraft data-fil
**Endast en YAML-fil behöver användats per Minecraft-värld oavsett hur många spelare det är som spelar.**
När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När detta
är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som innehåller allas data-filer.
Din data-fil borde ha en `.apmc` -extension.
Lägg din data-fil i dina forge-servrar `APData` -mapp. Se till att ta bort alla tidigare data-filer som var i där förut.
### Koppla till Multiservern
Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status
genom att skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient.
När du är inne i spelet, skriv `/connect <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
Archipelago-servern. `(<Lösenord>)` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt lösenord.
### Spela spelet
När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis
att du har lykats med att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla
in till din forge-server.

View File

@@ -85,5 +85,40 @@
]
}
]
},
{
"gameTitle": "Minecraft",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago Minecraft software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "minecraft/minecraft_en.md",
"link": "minecraft/minecraft/en",
"authors": [
"Kono Tyran"
]
},
{
"language": "Spanish",
"filename": "minecraft/minecraft_es.md",
"link": "minecraft/minecraft/es",
"authors": [
"Edos"
]
},
{
"language": "Swedish",
"filename": "minecraft/minecraft_sv.md",
"link": "minecraft/minecraft/sv",
"authors": [
"Albinum"
]
}
]
}
]
}
]

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2020 Archipelago</div>
<div id="copyright-notice">Copyright 2021 Archipelago</div>
<div id="links">
<a href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a>
-

View File

@@ -9,7 +9,7 @@
{% macro list_patches_room(room) %}
{% if room.seed.patches %}
<ul>
{% for patch in patches|list|sort(attribute="player") %}
{% for patch in room.seed.patches|list|sort(attribute="player_id") %}
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
{% endfor %}

View File

@@ -8,6 +8,7 @@ from uuid import UUID
from worlds.alttp import Items, Regions
from WebHostLib import app, cache, Room
from NetUtils import Hint
from Utils import restricted_loads
def get_id(item_name):
@@ -253,7 +254,7 @@ for item_name, data in Items.item_table.items():
big_key_ids[area] = data[2]
ids_big_key[data[2]] = area
from MultiServer import get_item_name_from_id
from MultiServer import get_item_name_from_id, Context
def attribute_item(inventory, team, recipient, item):
@@ -295,9 +296,9 @@ def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None)
if result:
return result
multidata = room.seed.multidata
multidata = Context._decompress(room.seed.multidata)
# in > 100 players this can take a bit of time and is the main reason for the cache
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
locations = multidata['locations']
names = multidata["names"]
seed_checks_in_area = checks_in_area.copy()
@@ -308,30 +309,24 @@ def get_static_room_data(room: Room):
for area, checks in key_only_locations.items():
seed_checks_in_area[area] += len(checks)
seed_checks_in_area["Total"] = 249
if "checks_in_area" not in multidata:
player_checks_in_area = {playernumber: (seed_checks_in_area if use_door_tracker and
(0x140031, playernumber) in locations else checks_in_area)
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: location_to_area
for playernumber in range(1, len(names[0]) + 1)}
else:
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][f'{playernumber}'][areaname])
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
for playernumber in range(1, len(names[0]) + 1)}
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
for playernumber in range(1, len(names[0]) + 1)}
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
for _, (item_id, item_player) in multidata["locations"]:
for _, (item_id, item_player) in locations.items():
if item_id in ids_big_key:
player_big_key_locations[item_player].add(ids_big_key[item_id])
if item_id in ids_small_key:
player_small_key_locations[item_player].add(ids_small_key[item_id])
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
player_big_key_locations, player_small_key_locations, multidata["precollected_items"]
_multidata_cache[room.seed.id] = result
return result
@@ -348,7 +343,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
abort(404)
# Collect seed information and pare it down to a single player
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, precollected_items = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1]
seed_checks_in_area = seed_checks_in_area[tracked_player]
location_to_area = player_location_to_area[tracked_player]
@@ -356,13 +351,18 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
checks_done = {loc_name: 0 for loc_name in default_locations}
# Add starting items to inventory
starting_items = room.seed.multidata.get("precollected_items", None)[tracked_player - 1]
starting_items = precollected_items[tracked_player]
if starting_items:
for item_id in starting_items:
attribute_item_solo(inventory, item_id)
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
# Add items to player inventory
for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}):
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}):
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
# Skip teams and players not matching the request
@@ -380,7 +380,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
checks_done["Total"] += 1
# Note the presence of the triforce item
for (ms_team, ms_player), game_state in room.multisave.get("client_game_state", []):
for (ms_team, ms_player), game_state in multisave.get("client_game_state", []):
# Skip teams and players not matching the request
if ms_team != tracked_team or ms_player != tracked_player:
continue
@@ -484,7 +484,8 @@ def getTracker(tracker: UUID):
room = Room.get(tracker=tracker)
if not room:
abort(404)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
player_small_key_locations, precollected_items = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)}
@@ -492,16 +493,20 @@ def getTracker(tracker: UUID):
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)}
precollected_items = room.seed.multidata.get("precollected_items", None)
hints = {team: set() for team in range(len(names))}
if "hints" in room.multisave:
for key, hintdata in room.multisave["hints"]:
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
if "hints" in multisave:
for key, hintdata in multisave["hints"]:
for hint in hintdata:
hints[key[0]].add(Hint(*hint))
for (team, player), locations_checked in room.multisave.get("location_checks", {}):
for (team, player), locations_checked in multisave.get("location_checks", {}):
if precollected_items:
precollected = precollected_items[player - 1]
precollected = precollected_items[player]
for item_id in precollected:
attribute_item(inventory, team, player, item_id)
for location in locations_checked:
@@ -513,7 +518,7 @@ def getTracker(tracker: UUID):
checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1
for (team, player), game_state in room.multisave.get("client_game_state", []):
for (team, player), game_state in multisave.get("client_game_state", []):
if game_state:
inventory[team][player][106] = 1 # Triforce
@@ -525,7 +530,7 @@ def getTracker(tracker: UUID):
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in room.multisave.get("client_activity_timers", []):
for (team, player), timestamp in multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
@@ -533,12 +538,12 @@ def getTracker(tracker: UUID):
for player, name in enumerate(names, 1):
player_names[(team, player)] = name
long_player_names = player_names.copy()
for (team, player), alias in room.multisave.get("name_aliases", []):
for (team, player), alias in multisave.get("name_aliases", []):
player_names[(team, player)] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
video = {}
for (team, player), data in room.multisave.get("video", []):
for (team, player), data in multisave.get("video", []):
video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,

View File

@@ -2,6 +2,7 @@ import json
import zlib
import zipfile
import logging
import MultiServer
from flask import request, flash, redirect, url_for, session, render_template
from pony.orm import commit, select
@@ -40,15 +41,18 @@ def uploads():
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
elif file.filename.endswith(".apbp"):
splitted = file.filename.split("/")[-1][3:].split("P", 1)
player = int(splitted[1].split(".")[0].split("_")[0])
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
player_id, player_name = splitted[1].split(".")[0].split("_")
patches.add(Patch(data=zfile.open(file, "r").read(), player_name=player_name, player_id=player_id))
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
elif file.filename.endswith(".archipelago"):
try:
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
multidata = zfile.open(file).read()
MultiServer.Context._decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
else:
multidata = zfile.open(file).read()
if multidata:
commit() # commit patches
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
@@ -61,9 +65,11 @@ def uploads():
flash("No multidata was found in the zip file, which is required.")
else:
try:
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
multidata = file.read()
MultiServer.Context._decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
raise
else:
seed = Seed(multidata=multidata, owner=session["_id"])
commit() # place into DB and generate ids

7
data/default.apsprite Normal file
View File

@@ -0,0 +1,7 @@
author: Nintendo
data: null
game: A Link to the Past
min_format_version: 1
name: Link
format_version: 1
sprite_version: 1

Binary file not shown.

View File

@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2021 Berserker55
Copyright (c) 2021 Berserker55 and Dewiniaid
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,108 +0,0 @@
-- for testing
script.on_event(defines.events.on_tick, function(event)
if event.tick%600 == 0 then
dumpTech()
end
end)
-- hook into researches done
script.on_event(defines.events.on_research_finished, function(event)
game.print("Research done")
dumpTech()
end)
function dumpTech()
local force = game.forces["player"]
local data_collection = {}
for tech_name, tech in pairs(force.technologies) do
if tech.researched and string.find(tech_name, "ap-") == 1 then
data_collection[tech_name] = tech.researched
end
end
game.write_file("research_done.json", game.table_to_json(data_collection), false)
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
-- game.print("Sent progress to Archipelago.")
end
function dumpGameInfo()
-- dump Game Information that the Archipelago Randomizer needs.
local data_collection = {}
local force = game.forces["player"]
for tech_name, tech in pairs(force.technologies) do
if tech.enabled then
local tech_data = {}
local unlocks = {}
tech_data["unlocks"] = unlocks
local requires = {}
tech_data["requires"] = requires
local ingredients = {}
tech_data["ingredients"] = ingredients
for tech_requirement, _ in pairs(tech.prerequisites) do
table.insert(requires, tech_requirement)
end
for _, modifier in pairs(tech.effects) do
if modifier.type == "unlock-recipe" then
table.insert(unlocks, modifier.recipe)
end
end
for _, ingredient in pairs(tech.research_unit_ingredients) do
table.insert(ingredients, ingredient.name)
end
data_collection[tech_name] = tech_data
end
game.write_file("techs.json", game.table_to_json(data_collection), false)
game.print("Exported Tech Data")
end
data_collection = {}
for recipe_name, recipe in pairs(force.recipes) do
local recipe_data = {}
recipe_data["ingredients"] = {}
recipe_data["products"] = {}
recipe_data["category"] = recipe.category
for _, ingredient in pairs(recipe.ingredients) do
table.insert(recipe_data["ingredients"], ingredient.name)
end
for _, product in pairs(recipe.products) do
table.insert(recipe_data["products"], product.name)
end
data_collection[recipe_name] = recipe_data
end
game.write_file("recipes.json", game.table_to_json(data_collection), false)
game.print("Exported Recipe Data")
-- data.raw can't be accessed from control.lua, need to find a better method
-- data_collection = {}
-- for machine_name, machine in pairs(data.raw["assembling_machine"]) do
-- local machine_data = {}
-- machine_data["categories"] = table.deepcopy(machine.crafting_categories)
-- data_collection[machine.name] = machine_data
-- end
-- game.write_file("machines.json", game.table_to_json(data_collection), false)
-- game.print("Exported Machine Data")
end
-- add / commands
commands.add_command("ap-get-info-dump", "Dump Game Info, used by Archipelago.", function(call)
dumpGameInfo()
end)
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
dumpTech()
end)
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
local force = game.forces["player"]
local tech_name = call.parameter
local tech = force.technologies[tech_name]
if tech ~= nil then
if tech.researched ~= true then
tech.researched = true
game.print({"", "Received ", tech.localised_name, " from Archipelago"})
game.play_sound({path="utility/research_completed"})
end
else
game.print("Unknown Technology " .. tech_name)
end
end)

View File

@@ -2,7 +2,7 @@
"name": "archipelago-client",
"version": "0.0.1",
"title": "Archipelago",
"author": "Berserker",
"author": "Berserker and Dewiniaid",
"homepage": "https://archipelago.gg",
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1"

23
data/factorio/mod/lib.lua Normal file
View File

@@ -0,0 +1,23 @@
function filter_ingredients(ingredients, ingredient_filter)
local new_ingredient_list = {}
for _, ingredient_table in pairs(ingredients) do
if ingredient_filter[ingredient_table[1]] then -- name of ingredient_table
table.insert(new_ingredient_list, ingredient_table)
end
end
return new_ingredient_list
end
function get_any_stack_size(name)
local item = game.item_prototypes[name]
if item ~= nil then
return item.stack_size
end
item = game.equipment_prototypes[name]
if item ~= nil then
return item.stack_size
end
-- failsafe
return 1
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -0,0 +1,237 @@
{% macro dict_to_lua(dict) -%}
{
{% for key, value in dict.items() %}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
{% endfor %}
}
{%- endmacro %}
require "lib"
require "util"
FREE_SAMPLES = {{ free_samples }}
SLOT_NAME = "{{ slot_name }}"
--SUPPRESS_INVENTORY_EVENTS = false
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
function on_force_created(event)
--event.force appears to be LuaForce.name, not LuaForce
game.forces[event.force].research_queue_enabled = true
local data = {}
data['earned_samples'] = {{ dict_to_lua(starting_items) }}
data["victory"] = 0
global.forcedata[event.force] = data
end
script.on_event(defines.events.on_force_created, on_force_created)
-- Destroy force data. This doesn't appear to be currently possible with the Factorio API, but here for completeness.
function on_force_destroyed(event)
global.forcedata[event.force.name] = nil
end
-- Initialize player data, either from them joining the game or them already being part of the game when the mod was
-- added.`
function on_player_created(event)
local player = game.players[event.player_index]
-- FIXME: This (probably) fires before any other mod has a chance to change the player's force
-- For now, they will (probably) always be on the 'player' force when this event fires.
local data = {}
data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples'])
global.playerdata[player.index] = data
update_player(player.index) -- Attempt to send pending free samples, if relevant.
end
script.on_event(defines.events.on_player_created, on_player_created)
function on_player_removed(event)
global.playerdata[event.player_index] = nil
end
script.on_event(defines.events.on_player_removed, on_player_removed)
function on_rocket_launched(event)
global.forcedata[event.rocket.force.name]['victory'] = 1
dumpInfo(event.rocket.force)
end
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
-- Updates a player, attempting to send them any pending samples (if relevant)
function update_player(index)
local player = game.players[index]
if not player or not player.valid then -- Do nothing if we reference an invalid player somehow
return
end
local character = player.character or player.cutscene_character
if not character or not character.valid then
return
end
local data = global.playerdata[index]
local samples = data['pending_samples']
local sent
--player.print(serpent.block(data['pending_samples']))
local stack = {}
--SUPPRESS_INVENTORY_EVENTS = true
for name, count in pairs(samples) do
stack.name = name
stack.count = count
if character.can_insert(stack) then
sent = character.insert(stack)
else
sent = 0
end
if sent > 0 then
player.print("Received " .. sent .. "x [item=" .. name .. "]")
data.suppress_full_inventory_message = false
end
if sent ~= count then -- Couldn't full send.
if not data.suppress_full_inventory_message then
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
end
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
samples[name] = count - sent -- Buffer the remaining items
break -- Stop trying to send other things
else
samples[name] = nil -- Remove from the list
end
end
--SUPPRESS_INVENTORY_EVENTS = false
end
-- Update players upon them connecting, since updates while they're offline are suppressed.
script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end)
function update_player_event(event)
--if not SUPPRESS_INVENTORY_EVENTS then
update_player(event.player_index)
--end
end
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
function add_samples(force, name, count)
local function add_to_table(t)
t[name] = (t[name] or 0) + count
end
-- Add to global table of earned samples for future new players
add_to_table(global.forcedata[force.name]['earned_samples'])
-- Add to existing players
for _, player in pairs(force.players) do
add_to_table(global.playerdata[player.index]['pending_samples'])
update_player(player.index)
end
end
script.on_init(function()
global.forcedata = {}
global.playerdata = {}
-- Fire dummy events for all currently existing forces.
local e = {}
for name, _ in pairs(game.forces) do
e.force = name
on_force_created(e)
end
e.force = nil
-- Fire dummy events for all currently existing players.
for index, _ in pairs(game.players) do
e.player_index = index
on_player_created(e)
end
end)
-- for testing
script.on_event(defines.events.on_tick, function(event)
if event.tick%600 == 300 then
dumpInfo(game.forces["player"])
end
end)
-- hook into researches done
script.on_event(defines.events.on_research_finished, function(event)
local technology = event.research
dumpInfo(technology.force)
if FREE_SAMPLES == 0 then
return -- Nothing else to do
end
if not technology.effects then
return -- No technology effects, so nothing to do.
end
for _, effect in pairs(technology.effects) do
if effect.type == "unlock-recipe" then
local recipe = game.recipe_prototypes[effect.recipe]
for _, result in pairs(recipe.products) do
if result.type == "item" and result.amount then
local name = result.name
local count
if FREE_SAMPLES == 1 then
count = result.amount
else
count = get_any_stack_size(result.name)
if FREE_SAMPLES == 2 then
count = math.ceil(count / 2)
end
end
add_samples(technology.force, name, count)
end
end
end
end
end)
function dumpInfo(force)
local research_done = {}
local data_collection = {
["research_done"] = research_done,
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
["slot_name"] = SLOT_NAME
}
for tech_name, tech in pairs(force.technologies) do
if tech.researched and string.find(tech_name, "ap%-") == 1 then
research_done[tech_name] = tech.researched
end
end
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
-- game.print("Sent progress to Archipelago.")
end
function chain_lookup(table, ...)
for _, k in ipairs{...} do
table = table[k]
if not table then
return nil
end
end
return table
end
-- add / commands
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
if call.player_index == nil then
dumpInfo(game.forces.player)
else
dumpInfo(game.players[call.player_index].force)
end
game.print("Wrote bridge file.")
end)
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
local force = game.forces["player"]
chunks = {}
for substring in call.parameter:gmatch("%S+") do -- split on " "
table.insert(chunks, substring)
end
local tech_name = chunks[1]
local source = chunks[2] or "Archipelago"
local tech = force.technologies[tech_name]
if tech ~= nil then
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
else
game.print("Unknown Technology " .. tech_name)
end
end)

View File

@@ -1,23 +1,50 @@
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib')
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }}
local technologies = data.raw["technology"]
local original_tech
local new_tree_copy
allowed_ingredients = {}
{%- for tech_name, technology in custom_data["custom_technologies"].items() %}
allowed_ingredients["{{ tech_name }}"] = {
{%- for ingredient in technology.ingredients %}
["{{ingredient}}"] = 1,
{%- endfor %}
}
{% endfor %}
local template_tech = table.deepcopy(technologies["automation"])
{#- ensure the copy unlocks nothing #}
template_tech.unlocks = {}
template_tech.upgrade = false
template_tech.effects = {}
template_tech.prerequisites = {}
function prep_copy(new_copy, old_tech)
old_tech.enabled = false
new_copy.unit = table.deepcopy(old_tech.unit)
local ingredient_filter = allowed_ingredients[old_tech.name]
if ingredient_filter ~= nil then
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
end
end
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player in locations %}
original_tech = technologies["{{original_tech_name}}"]
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
{#- hide and disable original tech; which will be shown, unlocked and enabled by AP Client #}
original_tech.enabled = false
{#- copy original tech costs #}
new_tree_copy.unit = table.deepcopy(original_tech.unit)
{% if item_name in tech_table %}
prep_copy(new_tree_copy, original_tech)
{% if tech_cost != 1 %}
if new_tree_copy.unit.count then
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
end
{% endif %}
{% if item_name in tech_table and visibility %}
{#- copy Factorio Technology Icon #}
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
@@ -28,7 +55,13 @@ new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
new_tree_copy.icons = nil
new_tree_copy.icon_size = 512
{% endif %}
{#- add new technology to game #}
{#- connect Technology #}
{%- if original_tech_name in tech_tree_layout_prerequisites %}
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
{% endfor %}
{% endif -%}
{#- add new Technology to game #}
data:extend{new_tree_copy}
{% endfor %}

View File

@@ -1,8 +1,18 @@
[technology-name]
{% for original_tech_name, item_name, receiving_player in locations %}
{%- if visibility -%}
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
{% else %}
ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
{%- endif -%}
{% endfor %}
[technology-description]
{% for original_tech_name, item_name, receiving_player in locations %}
"ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
{%- if visibility -%}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}.
{% else %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
{%- endif -%}
{% endfor %}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
author: Nintendo
data: null
game: A Link to the Past
min_format_version: 1
name: Link
format_version: 1
sprite_version: 1

View File

@@ -40,6 +40,8 @@ server_options:
# 1 -> Recommended for friendly racing, only allow Berserker's Multiworld, to disallow old /getitem for example
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
compatibility: 2
# log all server traffic, mostly for dev use
log_network: 0
# Options for MultiMystery.py
multi_mystery_options:
# Teams
@@ -112,5 +114,4 @@ lttp_options:
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
factorio_options:
executable: "factorio\\bin\\x64\\factorio"
script-output: "factorio\\script-output"
executable: "factorio\\bin\\x64\\factorio"

View File

@@ -52,7 +52,7 @@ Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks:
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
[UninstallDelete]
Type: dirifempty; Name: "{app}"

View File

@@ -52,7 +52,7 @@ Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks:
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
[UninstallDelete]
Type: dirifempty; Name: "{app}"

View File

@@ -27,6 +27,7 @@ game:
A Link to the Past: 1
Hollow Knight: 1
Factorio: 1
Minecraft: 1
# Shared Options supported by all games:
accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
@@ -35,6 +36,70 @@ accessibility:
progression_balancing:
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
# Can be uncommented to use it
# startinventory: # Begin the file with the listed items/upgrades
# Please only use items for the correct game, use triggers if need to be have seperated lists.
# Pegasus Boots: on
# Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4
# Factorio options:
tech_tree_layout:
single: 1
small_diamonds: 1
medium_diamonds: 1
pyramid: 1
funnel: 1
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0
military_science_pack: 0
chemical_science_pack: 0
production_science_pack: 0
utility_science_pack: 0
space_science_pack: 1
tech_cost:
very_easy : 0
easy : 0
kind : 0
normal : 1
hard : 0
very_hard : 0
insane : 0
free_samples:
none: 1
single_craft: 0
half_stack: 0
stack: 0
visibility:
none: 0
sending: 1
random_tech_ingredients:
on: 1
off: 0
starting_items:
burner-mining-drill: 19
stone-furnace: 19
# Minecraft options:
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
few: 0 # 30 advancements
normal: 1 # 50
many: 0 # 70
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
easy: 0
normal: 1
hard: 0
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
on: 0
off: 1
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
on: 0
off: 1
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
on: 0
off: 1
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
on: 0
off: 1
# A Link to the Past options:
### Logic Section ###
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version
@@ -87,7 +152,8 @@ progressive: # Enable or disable progressive items (swords, shields, bow)
entrance_shuffle:
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
restricted: 0 # Less strict than simple
full: 0 # Less strict than restricted
@@ -172,11 +238,9 @@ retro:
hints:
'on': 50 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips
weapons: # Specifically, swords
randomized: 0 # Swords are placed randomly throughout the world
assured: 50 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
item_pool:
easy: 0 # Doubled upgrades, progressives, and etc
normal: 50 # Item availability remains unchanged from vanilla game
@@ -235,12 +299,14 @@ beemizer: # Remove items from the global item pool and replace them with single
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
### Shop Settings ###
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50
5: 0
15: 0
30: 0
random: 0 # 0 to 30 evenly distributed
shop_shuffle:
none: 50
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
@@ -287,11 +353,6 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
# Can be uncommented to use it
# startinventory: # Begin the file with the listed items/upgrades
# Pegasus Boots: on
# Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4
glitch_boots:
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
off: 0
@@ -301,8 +362,8 @@ meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option
- inverted # Never play inverted seeds
retro:
- on # Never play retro seeds
weapons:
- swordless # Never play a swordless seed
swordless:
- on # Never play a swordless seed
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
@@ -326,9 +387,9 @@ linked_options:
- name: enemizer
options:
boss_shuffle: # Subchances can be injected too, which then get rolled
simple: 1
basic: 1
full: 1
random: 1
chaos: 1
singularity: 1
enemy_damage:
shuffled: 1
@@ -339,12 +400,46 @@ linked_options:
expert: 1
percentage: 0 # Set this to the percentage chance you want enemizer
# triggers that replace options upon rolling certain options
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
trigger_disabled: 50
randomized: 0 # Swords are placed randomly throughout the world
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
triggers:
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
- option_name: legacy_weapons
option_result: randomized
options:
swordless: off
- option_name: legacy_weapons
option_result: assured
options:
swordless: off
startinventory:
Progressive Sword: 1
- option_name: legacy_weapons
option_result: vanilla
options:
swordless: off
plando_items:
- items:
Progressive Sword: 4
locations:
- Master Sword Pedestal
- Pyramid Fairy - Left
- Blacksmith
- Link's Uncle
- option_name: legacy_weapons
option_result: swordless
options:
swordless: on
# end of legacy weapons block
- option_name: enemy_damage # targets enemy_damage
option_result: shuffled # if it rolls shuffled
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
swords: assured
swordless: off
### door rando only options (not supported at all yet on this branch) ###
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
vanilla: 50 # Everything should be like in vanilla

View File

@@ -2,9 +2,10 @@ colorama>=0.4.4
websockets>=8.1
PyYAML>=5.4.1
fuzzywuzzy>=0.18.0
bsdiff4>=1.2.0
bsdiff4>=1.2.1
prompt_toolkit>=3.0.18
appdirs>=1.4.4
maseya-z3pr>=1.0.0rc1
xxtea>=2.0.0.post0
factorio-rcon-py>=1.2.1
factorio-rcon-py>=1.2.1
jinja2>=2.11.3

View File

@@ -48,8 +48,10 @@ def manifest_creation():
path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path)
import json
from Utils import _version_tuple
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
"hashes": {path: hash.result() for path, hash in hashes.items()}}
"hashes": {path: hash.result() for path, hash in hashes.items()},
"version": _version_tuple}
json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest")
@@ -57,7 +59,6 @@ def manifest_creation():
scripts = {"LttPClient.py": "ArchipelagoLttPClient",
"MultiMystery.py": "ArchipelagoMultiMystery",
"MultiServer.py": "ArchipelagoServer",
"gui.py": "ArchipelagoLttPCreator",
"Mystery.py": "ArchipelagoMystery",
"LttPAdjuster.py": "ArchipelagoLttPAdjuster",
"FactorioClient.py": "ArchipelagoFactorioClient"}
@@ -67,7 +68,7 @@ exes = []
for script, scriptname in scripts.items():
exes.append(cx_Freeze.Executable(
script=script,
targetName=scriptname + ("" if sys.platform == "linux" else ".exe"),
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
icon=icon,
))
@@ -142,7 +143,7 @@ for file in os.listdir(alttpr_sprites_folder):
if signtool:
for exe in exes:
print(f"Signing {exe.targetName}")
os.system(signtool + exe.targetName)
print(f"Signing {exe.target_name}")
os.system(signtool + os.path.join(buildfolder, exe.target_name))
manifest_creation()

View File

@@ -1,9 +1,14 @@
import unittest
import pathlib
import Utils
file_path = pathlib.Path(__file__).parent.parent
Utils.local_path.cached_path = file_path
from BaseClasses import MultiWorld, CollectionState
from worlds.alttp.Items import ItemFactory
class TestBase(unittest.TestCase):
world: MultiWorld
_state_cache = {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
from test.minecraft.TestMinecraft import TestMinecraft
class TestEntrances(TestMinecraft):
def testPortals(self):
self.run_entrance_tests([
['Nether Portal', False, []],
['Nether Portal', False, [], ['Flint and Steel']],
['Nether Portal', False, [], ['Ingot Crafting']],
['Nether Portal', False, [], ['Progressive Tools']],
['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket']],
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']],
['End Portal', False, []],
['End Portal', False, [], ['Brewing']],
['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
['End Portal', False, [], ['Flint and Steel']],
['End Portal', False, [], ['Ingot Crafting']],
['End Portal', False, [], ['Progressive Tools']],
['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['End Portal', False, [], ['Progressive Weapons']],
['End Portal', False, [], ['Progressive Armor', 'Shield']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
])
def testStructures(self):
self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent
['Overworld Structure 1', False, []],
['Overworld Structure 1', False, [], ['Progressive Weapons']],
['Overworld Structure 1', False, [], ['Ingot Crafting', 'Campfire']],
['Overworld Structure 1', True, ['Progressive Weapons', 'Ingot Crafting']],
['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']],
['Overworld Structure 2', False, []],
['Overworld Structure 2', False, [], ['Progressive Weapons']],
['Overworld Structure 2', False, [], ['Ingot Crafting', 'Campfire']],
['Overworld Structure 2', True, ['Progressive Weapons', 'Ingot Crafting']],
['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']],
['Nether Structure 1', False, []],
['Nether Structure 1', False, [], ['Flint and Steel']],
['Nether Structure 1', False, [], ['Ingot Crafting']],
['Nether Structure 1', False, [], ['Progressive Tools']],
['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['Nether Structure 1', False, [], ['Progressive Weapons']],
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
['Nether Structure 2', False, []],
['Nether Structure 2', False, [], ['Flint and Steel']],
['Nether Structure 2', False, [], ['Ingot Crafting']],
['Nether Structure 2', False, [], ['Progressive Tools']],
['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['Nether Structure 2', False, [], ['Progressive Weapons']],
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
['The End Structure', False, []],
['The End Structure', False, [], ['Brewing']],
['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
['The End Structure', False, [], ['Flint and Steel']],
['The End Structure', False, [], ['Ingot Crafting']],
['The End Structure', False, [], ['Progressive Tools']],
['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['The End Structure', False, [], ['Progressive Weapons']],
['The End Structure', False, [], ['Progressive Armor', 'Shield']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
])

View File

@@ -0,0 +1,57 @@
from test.TestBase import TestBase
from BaseClasses import MultiWorld
from worlds.minecraft import minecraft_gen_item_pool
from worlds.minecraft.Regions import minecraft_create_regions, link_minecraft_structures
from worlds.minecraft.Rules import set_rules
from worlds.minecraft.Items import MinecraftItem, item_table
import Options
# Converts the name of an item into an item object
def MCItemFactory(items, player: int):
ret = []
singleton = False
if isinstance(items, str):
items = [items]
singleton = True
for item in items:
if item in item_table:
ret.append(MinecraftItem(item, item_table[item].progression, item_table[item].code, player))
else:
raise Exception(f"Unknown item {item}")
if singleton:
return ret[0]
return ret
class TestMinecraft(TestBase):
def setUp(self):
self.world = MultiWorld(1)
self.world.game[1] = "Minecraft"
exclusion_pools = ['hard', 'insane', 'postgame']
for pool in exclusion_pools:
setattr(self.world, f"include_{pool}_advancements", [False, False])
setattr(self.world, "advancement_goal", [0, Options.AdvancementGoal(value=0)])
setattr(self.world, "shuffle_structures", [False, False])
setattr(self.world, "combat_difficulty", [0, Options.CombatDifficulty(value=1)])
minecraft_create_regions(self.world, 1)
link_minecraft_structures(self.world, 1)
minecraft_gen_item_pool(self.world, 1)
set_rules(self.world, 1)
def _get_items(self, item_pool, all_except):
if all_except and len(all_except) > 0:
items = self.world.itempool[:]
items = [item for item in items if
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
items.extend(MCItemFactory(item_pool[0], 1))
else:
items = MCItemFactory(item_pool[0], 1)
return self.get_state(items)
def _get_items_partial(self, item_pool, missing_item):
new_items = item_pool[0].copy()
new_items.remove(missing_item)
items = MCItemFactory(new_items, 1)
return self.get_state(items)

View File

@@ -8,25 +8,31 @@ __all__ = {"lookup_any_item_id_to_name",
from .alttp.Items import lookup_id_to_name as alttp
from .hk.Items import lookup_id_to_name as hk
from .factorio import Technologies
lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name}
lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()}
from .minecraft.Items import lookup_id_to_name as mc
lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name, **mc}
assert len(alttp) + len(hk) + len(Technologies.lookup_id_to_name) + len(mc) == len(lookup_any_item_id_to_name)
lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()}
from .alttp import Regions
from .hk import Locations
from .minecraft import Locations as Advancements
lookup_any_location_id_to_name = {**Regions.lookup_id_to_name, **Locations.lookup_id_to_name,
**Technologies.lookup_id_to_name}
**Technologies.lookup_id_to_name, **Advancements.lookup_id_to_name}
assert len(Regions.lookup_id_to_name) + len(Locations.lookup_id_to_name) + \
len(Technologies.lookup_id_to_name) + len(Advancements.lookup_id_to_name) == \
len(lookup_any_location_id_to_name)
lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_id_to_name.items()}
network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name,
"lookup_any_item_id_to_name": lookup_any_item_id_to_name,
"version": 2}
"version": 6}
@enum.unique
class Games(str, enum.Enum):
HK = "Hollow Knight"
LTTP = "A Link to the Past"
Factorio = "Factorio"
Minecraft = "Minecraft"

View File

@@ -80,7 +80,7 @@ def KholdstareDefeatRule(state, player: int):
state.has('Fire Rod', player) or
(
state.has('Bombos', player) and
(state.has_sword(player) or state.world.swords[player] == 'swordless')
(state.has_sword(player) or state.world.swordless[player])
)
) and
(
@@ -89,7 +89,7 @@ def KholdstareDefeatRule(state, player: int):
(
state.has('Fire Rod', player) and
state.has('Bombos', player) and
state.world.swords[player] == 'swordless' and
state.world.swordless[player] and
state.can_extend_magic(player, 16)
)
)
@@ -113,7 +113,7 @@ def AgahnimDefeatRule(state, player: int):
def GanonDefeatRule(state, player: int):
if state.world.swords[player] == "swordless":
if state.world.swordless[player]:
return state.has('Hammer', player) and \
state.has_fire_source(player) and \
state.has('Silver Bow', player) and \
@@ -241,7 +241,7 @@ def place_bosses(world, player: int):
if shuffle_mode == "none":
return # vanilla bosses come pre-placed
if shuffle_mode in ["basic", "normal"]:
if shuffle_mode in ["basic", "full"]:
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random

View File

@@ -45,11 +45,9 @@ def parse_arguments(argv, no_defaults=False):
Requires the moon pearl to be Link in the Light World
instead of a bunny.
''')
parser.add_argument('--swords', default=defval('random'), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
parser.add_argument('--swordless', action='store_true',
help='''\
Select sword placement. (default: %(default)s)
Random: All swords placed randomly.
Assured: Start game with a sword already.
Toggles Swordless Mode
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock
@@ -57,7 +55,6 @@ def parse_arguments(argv, no_defaults=False):
Ether and Bombos Tablet can be activated with Hammer
(and Book). Bombos pads have been added in Ice
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'],
@@ -175,7 +172,7 @@ def parse_arguments(argv, no_defaults=False):
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
@@ -355,7 +352,6 @@ def parse_arguments(argv, no_defaults=False):
Torches means additionally easily accessible Torches that can be lit with Fire Rod are considered doable.
None means full traversal through dark rooms without tools is considered doable.''')
parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true")
parser.add_argument('--remote_items', default=defval(False), action='store_true')
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
@@ -401,7 +397,7 @@ def parse_arguments(argv, no_defaults=False):
for player in range(1, multiargs.multi + 1):
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer',
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
@@ -412,7 +408,7 @@ def parse_arguments(argv, no_defaults=False):
"triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots",
"required_medallions",
"plando_items", "plando_texts", "plando_connections", "er_seeds",
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
'restrict_dungeon_item_on_boss', 'reduceflashing', 'game',
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes', 'triforcehud']:

View File

@@ -60,6 +60,8 @@ def link_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.shuffle[player] == 'dungeonscrossed':
crossed_shuffle_dungeons(world, player)
elif world.shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player)
@@ -1117,7 +1119,7 @@ def link_inverted_entrances(world, player):
dw_entrances = list(Inverted_DW_Dungeon_Entrances)
# randomize which desert ledge door is a must-exit
if world.random.randint(0, 1) == 0:
if world.random.randint(0, 1):
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)')
lw_entrances.append('Desert Palace Entrance (West)')
else:
@@ -1152,7 +1154,7 @@ def link_inverted_entrances(world, player):
if aga_door in lw_entrances:
lw_entrances.remove(aga_door)
elif aga_door in dw_entrances:
else:
dw_entrances.remove(aga_door)
connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
@@ -1161,7 +1163,8 @@ def link_inverted_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.shuffle[player] == 'dungeonscrossed':
inverted_crossed_shuffle_dungeons(world, player)
elif world.shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player)
@@ -1368,11 +1371,8 @@ def link_inverted_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
caves.append(tuple(world.random.sample(
['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3)))
lw_entrances.append('Hyrule Castle Entrance (South)')
if not world.shuffle_ganon:
connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
@@ -1386,7 +1386,7 @@ def link_inverted_entrances(world, player):
aga_doors = [i for i in all_entrances_aga]
world.random.shuffle(aga_doors)
aga_door = aga_doors.pop()
if aga_door in hc_ledge_entrances:
lw_entrances.remove(aga_door)
hc_ledge_entrances.remove(aga_door)
@@ -1403,7 +1403,7 @@ def link_inverted_entrances(world, player):
connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
caves.remove('Inverted Agahnims Tower Exit')
# place links house
links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
@@ -1433,11 +1433,14 @@ def link_inverted_entrances(world, player):
except ValueError:
pass
else: # if the cave wasn't placed we get here
connect_caves(world, lw_entrances, [], old_man_house, player)
connect_caves(world, lw_entrances, [], old_man_house, player)
else:
connect_caves(world, dw_entrances, [], old_man_house, player)
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
# put all HC exits in LW in inverted full shuffle
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances]
@@ -2027,17 +2030,10 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player):
world.random.shuffle(lw_entrances)
world.random.shuffle(dw_entrances)
world.random.shuffle(caves)
while caves:
# connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill
cave_candidate = (None, 0)
for i, cave in enumerate(caves):
if isinstance(cave, str):
cave = (cave,)
if len(cave) > cave_candidate[1]:
cave_candidate = (i, len(cave))
cave = caves.pop(cave_candidate[0])
target = lw_entrances if world.random.randint(0, 1) == 0 else dw_entrances
# connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill
caves.sort(key=lambda cave: 1 if isinstance(cave, str) else len(cave), reverse=True)
for cave in caves:
target = lw_entrances if world.random.randint(0, 1) else dw_entrances
if isinstance(cave, str):
cave = (cave,)
@@ -2048,6 +2044,7 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player):
for exit in cave:
connect_two_way(world, target.pop(), exit, player)
caves.clear() # emulating old behaviour of popping caves from the list in-place
def connect_doors(world, doors, targets, player):
@@ -2200,6 +2197,90 @@ def simple_shuffle_dungeons(world, player):
connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player)
connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player)
def crossed_shuffle_dungeons(world, player: int):
lw_entrances = LW_Dungeon_Entrances.copy()
dw_entrances = DW_Dungeon_Entrances.copy()
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
skull_woods_shuffle(world, player)
dungeon_exits = Dungeon_Exits_Base.copy()
dungeon_entrances = lw_entrances+dw_entrances
if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
dungeon_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
dungeon_entrances.append('Hyrule Castle Entrance (South)')
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
if world.mode[player] == 'standard':
connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
assert not dungeon_exits # make sure all exits are accounted for
def inverted_crossed_shuffle_dungeons(world, player: int):
lw_entrances = Inverted_LW_Dungeon_Entrances.copy()
dw_entrances = Inverted_DW_Dungeon_Entrances.copy()
lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit)
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
skull_woods_shuffle(world, player)
dungeon_exits = Inverted_Dungeon_Exits_Base.copy()
dungeon_entrances = lw_entrances+dw_entrances
# randomize which desert ledge door is a must-exit
if world.random.randint(0, 1):
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)')
dungeon_entrances.append('Desert Palace Entrance (West)')
else:
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (West)')
dungeon_entrances.append('Desert Palace Entrance (North)')
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
dungeon_entrances.append('Hyrule Castle Entrance (South)')
if not world.shuffle_ganon:
connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
else:
dungeon_entrances.append('Inverted Ganons Tower')
dungeon_exits.append('Inverted Ganons Tower Exit')
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower']
# shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit
world.random.shuffle(dungeon_entrances)
aga_door = dungeon_entrances.pop()
if aga_door in hc_ledge_entrances:
hc_ledge_entrances.remove(aga_door)
world.random.shuffle(hc_ledge_entrances)
hc_ledge_must_exit = hc_ledge_entrances.pop()
dungeon_entrances.remove(hc_ledge_must_exit)
lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit)
connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
dungeon_exits.remove('Inverted Agahnims Tower Exit')
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
assert not dungeon_exits # make sure all exits are accounted for
def unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits):
def shuffle_lists_in_list(ls):

View File

@@ -70,7 +70,7 @@ difficulties = {
basicmagic=['Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'],
progressivesword=['Progressive Sword'] * 8,
basicsword=['Master Sword', 'Tempered Sword', 'Golden Sword', 'Fighter Sword'] * 2,
progressivebow=["Progressive Bow"] * 2,
progressivebow=["Progressive Bow"] * 4,
basicbow=['Bow', 'Silver Bow'] * 2,
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -279,7 +279,7 @@ def generate_itempool(world, player: int):
itempool.extend(itemdiff.bottles)
itempool.extend(itemdiff.basicbow)
itempool.extend(itemdiff.basicarmor)
if world.swords[player] != 'swordless':
if not world.swordless[player]:
itempool.extend(itemdiff.basicsword)
itempool.extend(itemdiff.basicmagic)
itempool.extend(itemdiff.basicglove)
@@ -335,7 +335,7 @@ def generate_itempool(world, player: int):
possible_weapons = []
for item in pool:
if item in ['Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']:
if not found_sword and world.swords[player] != 'swordless':
if not found_sword:
found_sword = True
possible_weapons.append(item)
if item in ['Progressive Bow', 'Bow'] and not found_bow:
@@ -548,7 +548,7 @@ def get_pool_core(world, player: int):
timer = world.timer[player]
goal = world.goal[player]
mode = world.mode[player]
swords = world.swords[player]
swordless = world.swordless[player]
retro = world.retro[player]
logic = world.logic[player]
@@ -619,7 +619,7 @@ def get_pool_core(world, player: int):
if want_progressives():
pool.extend(diff.progressivebow)
elif (swords == 'swordless' or logic == 'noglitches') and goal != 'icerodhunt':
elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
swordless_bows = ['Bow', 'Silver Bow']
if difficulty == "easy":
swordless_bows *= 2
@@ -627,33 +627,11 @@ def get_pool_core(world, player: int):
else:
pool.extend(diff.basicbow)
if swords == 'swordless':
if swordless:
pool.extend(diff.swordless)
elif swords == 'vanilla':
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
world.random.shuffle(swords_to_use)
place_item('Link\'s Uncle', swords_to_use.pop())
place_item('Blacksmith', swords_to_use.pop())
place_item('Pyramid Fairy - Left', swords_to_use.pop())
if goal != 'pedestal':
place_item('Master Sword Pedestal', swords_to_use.pop())
else:
swords_to_use.pop()
place_item('Master Sword Pedestal', 'Triforce')
if swords_to_use:
pool.extend(swords_to_use)
else:
progressive_swords = want_progressives()
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
if swords == 'assured' and goal != 'icerodhunt':
if progressive_swords:
precollected_items.append('Progressive Sword')
pool.remove('Progressive Sword')
else:
precollected_items.append('Fighter Sword')
pool.remove('Fighter Sword')
pool.extend(['Rupees (50)'])
extraitems = total_items_to_place - len(pool) - len(placed_items)
@@ -684,7 +662,7 @@ def get_pool_core(world, player: int):
else:
break
if goal == 'pedestal' and swords != 'vanilla':
if goal == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
pool.remove("Rupees (20)")

View File

@@ -676,10 +676,12 @@ location_table: typing.Dict[str,
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, -1: "cheat console"}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()},
-1: "Cheat Console", -2: "Server"}
lookup_id_to_name.update(shop_table_by_location_id)
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, "cheat console": -1}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()},
"Cheat Console": -1, "Server": -2}
lookup_name_to_id.update(shop_table_by_location)
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',

View File

@@ -14,6 +14,7 @@ import subprocess
import threading
import xxtea
import concurrent.futures
import bsdiff4
from typing import Optional
from BaseClasses import CollectionState, Region
@@ -22,10 +23,12 @@ from worlds.alttp.Shops import ShopType
from worlds.alttp.Dungeons import dungeon_music_addresses
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, \
from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
Blind_texts, \
BombShop2_texts, junk_texts
from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, \
from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \
DeathMountain_texts, \
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_bundled
@@ -41,6 +44,7 @@ except:
enemizer_logger = logging.getLogger("Enemizer")
class LocalRom(object):
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
@@ -428,8 +432,11 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli):
except OSError:
pass
tile_list_lock = threading.Lock()
_tile_collection_table = []
def _populate_tile_sets():
with tile_list_lock:
if not _tile_collection_table:
@@ -442,6 +449,7 @@ def _populate_tile_sets():
for file in os.listdir(dir):
pool.submit(load_tileset_from_file, os.path.join(dir, file))
class TileSet:
def __init__(self, filename):
with open(filename, 'rt', encoding='utf-8-sig') as file:
@@ -497,25 +505,24 @@ def _populate_sprite_table():
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
class Sprite(object):
palette = (255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 71, 54, 104, 59, 74, 10, 239, 18, 92, 42, 113, 21, 24, 122,
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 128, 105, 145, 118, 184, 38, 127, 67, 92, 42, 153, 17, 24, 122,
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 87, 16, 126, 69, 243, 109, 185, 126, 92, 42, 39, 34, 24, 122,
255, 127, 126, 35, 218, 17, 158, 54, 165, 20, 255, 1, 120, 16, 151,
61, 71, 54, 104, 59, 74, 10, 239, 18, 126, 86, 114, 24, 24, 122)
glove_palette = (246, 82, 118, 3)
class Sprite():
sprite_size = 28672
palette_size = 120
glove_size = 4
author_name: Optional[str] = None
base_data: bytes
def __init__(self, filename):
if not hasattr(Sprite, "base_data"):
self.get_vanilla_sprite_data()
with open(filename, 'rb') as file:
filedata = bytearray(file.read())
filedata = file.read()
self.name = os.path.basename(filename)
self.valid = True
if len(filedata) == 0x7000:
if filename.endswith(".apsprite"):
self.from_ap_sprite(filedata)
elif len(filedata) == 0x7000:
# sprite file with graphics and without palette data
self.sprite = filedata[:0x7000]
elif len(filedata) == 0x7078:
@@ -534,26 +541,79 @@ class Sprite(object):
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
self.from_zspr(filedata, filename)
else:
self.valid = False
def get_vanilla_sprite_data(self):
from Patch import get_base_rom_path
file_name = get_base_rom_path()
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
filedata = filedata.decode("utf-8-sig")
import yaml
obj = yaml.safe_load(filedata)
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
@property
def author_game_display(self) -> str:
name = getattr(self, "_author_game_display", "")
if not name:
name = self.author_name
# At this point, may need some filtering to displayable characters
return name
def to_ap_sprite(self, path):
from .. import Games
import yaml
payload = {"format_version": 1,
"min_format_version": 1,
"sprite_version": 1,
"name": self.name,
"author": self.author_name,
"game": Games.LTTP.value,
"data": self.get_delta()}
with open(path, "w") as f:
f.write(yaml.safe_dump(payload))
def get_delta(self):
modified_data = self.sprite + self.palette + self.glove_palette
return bsdiff4.diff(Sprite.base_data, modified_data)
def from_zspr(self, filedata, filename):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
@@ -569,7 +629,7 @@ class Sprite(object):
@staticmethod
def default_link_sprite():
return Sprite(local_path('../../data', 'default.zspr'))
return Sprite(local_path('data', 'default.apsprite'))
def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)]
@@ -602,13 +662,14 @@ class Sprite(object):
arr[y + 8][x + 8] = bottom_right[y][x]
return arr
def parse_zspr(self, filedata, expected_kind):
logger = logging.getLogger('')
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger('ZSPR')
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
return None
(version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from(
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error('Error parsing ZSPR file: Version %g not supported', version)
@@ -631,6 +692,7 @@ class Sprite(object):
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
@@ -645,7 +707,7 @@ class Sprite(object):
logger.error('Error parsing ZSPR file: Unexpected end of file')
return None
return (sprite, palette, sprite_name, author_name)
return (sprite, palette, sprite_name, author_name, author_credits_name)
def decode_palette(self):
"Returns the palettes as an array of arrays of 15 colors"
@@ -657,7 +719,7 @@ class Sprite(object):
return pair[1] << 8 | pair[0]
def expand_color(i):
return ((i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8)
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
# turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
@@ -689,6 +751,14 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028,
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3,
0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
def get_nonnative_item_sprite(game):
game_to_id = {
"Factorio": 0x09, # Hammer
"Hollow Knight": 0x21, # Bug Catching Net
"Minecraft": 0x13, # Shovel
}
return game_to_id.get(game, 0x6B) # default to Power Star
def patch_rom(world, rom, player, team, enemized):
local_random = world.rom_seeds[player]
@@ -711,10 +781,7 @@ def patch_rom(world, rom, player, team, enemized):
if location.item is not None:
if location.item.game != "A Link to the Past":
if location.item.game == "Factorio":
itemid = 0x09 # Hammer Sprite
else:
itemid = 0x21 # Bug Catching Net
itemid = get_nonnative_item_sprite(location.item.game)
# Keys in their native dungeon should use the orignal item code for keys
elif location.parent_region.dungeon:
if location.parent_region.dungeon.is_dungeon_item(location.item):
@@ -751,7 +818,6 @@ def patch_rom(world, rom, player, team, enemized):
for music_address in music_addresses:
rom.write_byte(music_address, music)
if world.mapshuffle[player]:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
@@ -879,7 +945,7 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
# set light cones
rom.write_byte(0x180038, 0x01 if world.sewer_light_cone[player] else 0x00)
rom.write_byte(0x180038, 0x01 if world.mode[player] == "standard" else 0x00)
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
@@ -961,7 +1027,7 @@ def patch_rom(world, rom, player, team, enemized):
# Set overflow items for progressive equipment
rom.write_bytes(0x180090,
[difficulty.progressive_sword_limit if world.swords[player] != 'swordless' else 0,
[difficulty.progressive_sword_limit if not world.swordless[player] else 0,
overflow_replacement,
difficulty.progressive_shield_limit, overflow_replacement,
difficulty.progressive_armor_limit, overflow_replacement,
@@ -971,7 +1037,7 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement])
if difficulty.progressive_bow_limit < 2 and (
world.swords[player] == 'swordless' or world.logic[player] == 'noglitches'):
world.swordless[player] or world.logic[player] == 'noglitches'):
rom.write_bytes(0x180098, [2, overflow_replacement])
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
@@ -1070,7 +1136,6 @@ def patch_rom(world, rom, player, team, enemized):
byte = int(rom.read_byte(address))
rom.write_byte(address, prize_replacements.get(byte, byte))
# Fill in item substitutions table
rom.write_bytes(0x184000, [
# original_item, limit, replacement_item, filler
@@ -1123,11 +1188,11 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x180029, 0x01) # Smithy quick item give
# set swordless mode settings
rom.write_byte(0x18003F, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if world.swords[player] == 'swordless' else 0x00) # open curtains
rom.write_byte(0x180041, 0x01 if world.swords[player] == 'swordless' else 0x00) # swordless medallions
rom.write_byte(0x180043, 0xFF if world.swords[player] == 'swordless' else 0x00) # starting sword for link
rom.write_byte(0x180044, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer activates tablets
rom.write_byte(0x18003F, 0x01 if world.swordless[player] else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if world.swordless[player] else 0x00) # open curtains
rom.write_byte(0x180041, 0x01 if world.swordless[player] else 0x00) # swordless medallions
rom.write_byte(0x180043, 0xFF if world.swordless[player] else 0x00) # starting sword for link
rom.write_byte(0x180044, 0x01 if world.swordless[player] else 0x00) # hammer activates tablets
if world.item_functionality[player] == 'easy':
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
@@ -1146,9 +1211,12 @@ def patch_rom(world, rom, player, team, enemized):
# Set up requested clock settings
if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']:
rom.write_int32(0x180200, world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x180200,
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204,
world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208,
world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
else:
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
@@ -1507,7 +1575,8 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0xEFD95, digging_game_rng)
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[
player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
# remove shield from uncle
@@ -1579,7 +1648,6 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
write_strings(rom, world, player, team)
rom.write_byte(0x18637C, 1 if world.remote_items[player] else 0)
@@ -1650,8 +1718,21 @@ def write_custom_shops(rom, world, player):
slot = 0 if shop.type == ShopType.TakeAny else index
if item is None:
break
if not item['item'] in item_table: # item not native to ALTTP
item_code = 0x21
if not item['item'] in item_table: # item not native to ALTTP
# This is a terrible way to do this, please fix later
from worlds.hk.Items import lookup_id_to_name as hk_lookup
from worlds.factorio.Technologies import lookup_id_to_name as factorio_lookup
from worlds.minecraft.Items import lookup_id_to_name as mc_lookup
item_name = item['item']
if item_name in hk_lookup.values():
item_game = 'Hollow Knight'
elif item_name in factorio_lookup.values():
item_game = 'Factorio'
elif item_name in mc_lookup.values():
item_game = 'Minecraft'
else:
item_game = 'Generic'
item_code = get_nonnative_item_sprite(item_game)
else:
item_code = ItemFactory(item['item'], player).code
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
@@ -1691,7 +1772,8 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False, triforcehud:str = None):
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None):
local_random = random if not world else world.rom_seeds[player]
# enable instant item menu
@@ -1716,22 +1798,22 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
else:
rom.write_byte(0x180048, 0x08)
# Reduce flashing by nopping out instructions
if reduceflashing:
rom.write_bytes(0x17E07, [0x06]) # reduce amount of colors changed, add this branch if we need to reduce more ""+ [0x80] + [(0x81-0x08)]""
rom.write_bytes(0x17EAB, [0xD0, 0x03, 0xA9, 0x40, 0x29, 0x60]) # nullifies aga lightning, cutscene, vitreous, bat, ether
rom.write_bytes(0x17E07, [
0x06]) # reduce amount of colors changed, add this branch if we need to reduce more ""+ [0x80] + [(0x81-0x08)]""
rom.write_bytes(0x17EAB,
[0xD0, 0x03, 0xA9, 0x40, 0x29, 0x60]) # nullifies aga lightning, cutscene, vitreous, bat, ether
# ONLY write to black values with this low pale blue to indicate flashing, that's IT. ""BNE + : LDA #$2940 : + : RTS""
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
rom.write_bytes(0x3FA7B, [0x80, 0xac-0x7b]) # branch from palette writing lightning on death mountain
rom.write_byte(0x10817F, 0x01) # internal rom option
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
rom.write_bytes(0x3FA7B, [0x80, 0xac - 0x7b]) # branch from palette writing lightning on death mountain
rom.write_byte(0x10817F, 0x01) # internal rom option
else:
rom.write_bytes(0x17E07, [0x00])
rom.write_bytes(0x17E07, [0x00])
rom.write_bytes(0x17EAB, [0x85, 0x00, 0x29, 0x1F, 0x00, 0x18])
rom.write_bytes(0x123FE, [0x32]) # original weather flash value
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
rom.write_byte(0x10817F, 0x00) # internal rom option
rom.write_bytes(0x123FE, [0x32]) # original weather flash value
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
rom.write_byte(0x10817F, 0x00) # internal rom option
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
@@ -1766,7 +1848,8 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
if triforcehud:
# set triforcehud
triforce_flag = (rom.read_byte(0x180167) & 0x80) | {'normal': 0x00, 'hide_goal': 0x01, 'hide_required': 0x02, 'hide_both': 0x03}[triforcehud]
triforce_flag = (rom.read_byte(0x180167) & 0x80) | \
{'normal': 0x00, 'hide_goal': 0x01, 'hide_required': 0x02, 'hide_both': 0x03}[triforcehud]
rom.write_byte(0x180167, triforce_flag)
if z3pr:
@@ -1775,7 +1858,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
option_name: True
}
data_dir = local_path("../../data") if is_bundled() else None
data_dir = local_path("data") if is_bundled() else None
offsets_array = build_offset_collections(options, data_dir)
restore_maseya_colors(rom, offsets_array)
if mode == 'default':
@@ -2014,7 +2097,7 @@ def write_strings(rom, world, player, team):
tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting.
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']:
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
@@ -2045,7 +2128,7 @@ def write_strings(rom, world, player, team):
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
# Bottle Vendor hint
vendor_location = world.get_location("Bottle Merchant", player)
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?"\
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
f"\n ≥ I want\n no way!\n{{CHOICE}}"
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
@@ -2072,7 +2155,7 @@ def write_strings(rom, world, player, team):
break
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
hint_count = 0
elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
hint_count = 2
@@ -2114,7 +2197,7 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else:
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 0
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
if hint_count:
@@ -2128,10 +2211,10 @@ def write_strings(rom, world, player, team):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy()
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
locations_to_hint.extend(InconvenientVanillaLocations)
local_random.shuffle(locations_to_hint)
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 5
for location in locations_to_hint[:hint_count]:
if location == 'Swamp Left':
if local_random.randint(0, 1):
@@ -2190,7 +2273,7 @@ def write_strings(rom, world, player, team):
if world.bigkeyshuffle[player]:
items_to_hint.extend(BigKeys)
local_random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8
while hint_count > 0 and items_to_hint:
this_item = items_to_hint.pop(0)
this_location = world.find_items(this_item, player)
@@ -2218,7 +2301,7 @@ def write_strings(rom, world, player, team):
prog_bow_locs = world.find_items('Progressive Bow', player)
distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None)
progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
world.swords[player] == 'swordless' or world.logic[player] == 'noglitches')
world.swordless[player] or world.logic[player] == 'noglitches')
if distinguished_prog_bow_loc:
prog_bow_locs.remove(distinguished_prog_bow_loc)
silverarrow_hint = (' %s?' % hint_text(distinguished_prog_bow_loc).replace('Ganon\'s',
@@ -2394,7 +2477,7 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(0xDC21D, 0x6B) # inverted mode flute activation (skip weathervane overlay)
rom.write_bytes(0x48DB3, [0xF8, 0x01]) # inverted mode (bird X)
rom.write_byte(0x48D5E, 0x01) # inverted mode (rock X)
rom.write_bytes(0x48CC1+36, bytes([0xF8]*12)) # (rock X)
rom.write_bytes(0x48CC1 + 36, bytes([0xF8] * 12)) # (rock X)
rom.write_int16s(snes_to_pc(0x02E849),
[0x0043, 0x0056, 0x0058, 0x006C, 0x006F, 0x0070, 0x007B, 0x007F, 0x001B]) # dw flute
rom.write_int16(snes_to_pc(0x02E8D5), 0x07C8)
@@ -2407,7 +2490,7 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01)
@@ -2465,7 +2548,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']:
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
@@ -2528,7 +2611,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
@@ -2565,10 +2648,10 @@ def set_inverted_mode(world, player, rom):
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)

View File

@@ -505,7 +505,7 @@ def default_rules(world, player):
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
if world.swords[player] == 'swordless':
if world.swordless[player]:
swordless_rules(world, player)
@@ -657,7 +657,7 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
if world.swords[player] == 'swordless':
if world.swordless[player]:
swordless_rules(world, player)
def no_glitches_rules(world, player):
@@ -781,7 +781,7 @@ def add_conditional_lamps(world, player):
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
if not world.sewer_light_cone[player]:
if not world.mode[player] == "standard":
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
@@ -916,7 +916,9 @@ def set_trock_key_rules(world, player):
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
world.push_item(world.get_location('Turtle Rock - Big Key Chest', player), ItemFactory('Small Key (Turtle Rock)', player), False)
item = ItemFactory('Small Key (Turtle Rock)', player)
item.world = world
world.push_item(world.get_location('Turtle Rock - Big Key Chest', player), item, False)
world.get_location('Turtle Rock - Big Key Chest', player).event = True
toss_junk_item(world, player)

View File

@@ -218,7 +218,9 @@ def ShopSlotFill(world):
continue
item_name = location.item.name
if any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
if location.item.game != "A Link to the Past":
price = world.random.randrange(1, 28)
elif any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
price = world.random.randrange(1, 7)
elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
price = world.random.randrange(2, 14)

View File

@@ -91,25 +91,6 @@ from BaseClasses import Location, Item
# self.dark_room_logic = "lamp"
# self.restrict_dungeon_item_on_boss = False
#
# @property
# def sewer_light_cone(self):
# return self.mode == "standard"
#
# @property
# def fix_trock_doors(self):
# return self.shuffle != 'vanilla' or self.mode == 'inverted'
#
# @property
# def fix_skullwoods_exit(self):
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
#
# @property
# def fix_palaceofdarkness_exit(self):
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
#
# @property
# def fix_trock_exit(self):
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
class ALttPLocation(Location):

View File

@@ -1,6 +1,7 @@
"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
import os
import zipfile
from typing import Optional
import threading
import json
@@ -8,6 +9,7 @@ import json
import jinja2
import Utils
import shutil
import Options
from BaseClasses import MultiWorld
from .Technologies import tech_table
@@ -25,30 +27,65 @@ base_info = {
"factorio_version": "1.1"
}
def generate_mod(world: MultiWorld, player: int):
# TODO: clean this up, probably as a jinja macro; then add logic for the recipes in completion condition
rocket_recipes = {
Options.MaxSciencePack.option_space_science_pack:
'{{"rocket-control-unit", 10}, {"low-density-structure", 10}, {"rocket-fuel", 10}}',
Options.MaxSciencePack.option_utility_science_pack:
'{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
Options.MaxSciencePack.option_production_science_pack:
'{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
Options.MaxSciencePack.option_chemical_science_pack:
'{{"advanced-circuit", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
Options.MaxSciencePack.option_military_science_pack:
'{{"defender-capsule", 10}, {"stone-wall", 10}, {"coal", 10}}',
Options.MaxSciencePack.option_logistic_science_pack:
'{{"electronic-circuit", 10}, {"stone-brick", 10}, {"coal", 10}}',
Options.MaxSciencePack.option_automation_science_pack:
'{{"copper-cable", 10}, {"iron-plate", 10}, {"wood", 10}}'
}
def generate_mod(world: MultiWorld, player: int, seedname: str):
global template, locale_template
with template_load_lock:
if not template:
template = jinja2.Template(open(Utils.local_path("data", "factorio", "mod_template", "data-final-fixes.lua")).read())
locale_template = jinja2.Template(open(Utils.local_path("data", "factorio", "mod_template", "locale", "en", "locale.cfg")).read())
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
template = jinja2.Template(open(os.path.join(mod_template_folder, "data-final-fixes.lua")).read())
locale_template = jinja2.Template(open(os.path.join(mod_template_folder, "locale", "en", "locale.cfg")).read())
control_template = jinja2.Template(open(os.path.join(mod_template_folder, "control.lua")).read())
# get data for templates
player_names = {x: world.player_names[x][0] for x in world.player_ids}
locations = []
for location in world.get_filled_locations(player):
if not location.name.startswith("recipe-"): # introduce this a new location property?
locations.append((location.name, location.item.name, location.item.player))
mod_name = f"archipelago-client-{world.seed}-{player}"
locations.append((location.name, location.item.name, location.item.player))
mod_name = f"AP-{seedname}-P{player}-{world.player_names[player][0]}"
tech_cost = {0: 0.1,
1: 0.25,
2: 0.5,
3: 1,
4: 2,
5: 5,
6: 10}[world.tech_cost[player].value]
template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table,
"mod_name": mod_name}
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost, "custom_data": world.custom_data[player],
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player],
"rocket_recipe" : rocket_recipes[world.max_science_pack[player].value],
"slot_name": world.player_names[player][0],
"starting_items": world.starting_items[player]}
for factorio_option in Options.factorio_options:
template_data[factorio_option] = getattr(world, factorio_option)[player].value
control_code = control_template.render(**template_data)
data_final_fixes_code = template.render(**template_data)
mod_code = template.render(**template_data)
mod_dir = Utils.output_path(mod_name)
mod_dir = Utils.output_path(mod_name)+"_"+Utils.__version__
en_locale_dir = os.path.join(mod_dir, "locale", "en")
os.makedirs(en_locale_dir, exist_ok=True)
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
f.write(mod_code)
f.write(data_final_fixes_code)
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
f.write(control_code)
locale_content = locale_template.render(**template_data)
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
f.write(locale_content)
@@ -56,3 +93,14 @@ def generate_mod(world: MultiWorld, player: int):
info["name"] = mod_name
with open(os.path.join(mod_dir, "info.json"), "wt") as f:
json.dump(info, f, indent=4)
# zip the result
zf_path = os.path.join(mod_dir+".zip")
with zipfile.ZipFile(zf_path, compression=zipfile.ZIP_DEFLATED, mode='w') as zf:
for root, dirs, files in os.walk(mod_dir):
for file in files:
zf.write(os.path.join(root, file),
os.path.relpath(os.path.join(root, file),
os.path.join(mod_dir, '..')))
shutil.rmtree(mod_dir)

95
worlds/factorio/Shapes.py Normal file
View File

@@ -0,0 +1,95 @@
from typing import Dict, List, Set
from BaseClasses import MultiWorld
from Options import TechTreeLayout
from worlds.factorio.Technologies import technology_table
def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
prerequisites: Dict[str, Set[str]] = {}
layout = world.tech_tree_layout[player].value
custom_technologies = world.custom_data[player]["custom_technologies"]
if layout == TechTreeLayout.option_small_diamonds:
slice_size = 4
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
while len(tech_names) > slice_size:
slice = tech_names[:slice_size]
tech_names = tech_names[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients))
diamond_0, diamond_1, diamond_2, diamond_3 = slice
# 0 |
# 1 2 |
# 3 V
prerequisites[diamond_3] = {diamond_1, diamond_2}
prerequisites[diamond_2] = prerequisites[diamond_1] = {diamond_0}
elif layout == TechTreeLayout.option_medium_diamonds:
slice_size = 9
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
while len(tech_names) > slice_size:
slice = tech_names[:slice_size]
tech_names = tech_names[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients))
# 0 |
# 1 2 |
# 3 4 5 |
# 6 7 |
# 8 V
prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]}
prerequisites[slice[3]] = {slice[1]}
prerequisites[slice[4]] = {slice[1], slice[2]}
prerequisites[slice[5]] = {slice[2]}
prerequisites[slice[6]] = {slice[3], slice[4]}
prerequisites[slice[7]] = {slice[4], slice[5]}
prerequisites[slice[8]] = {slice[6], slice[7]}
elif layout == TechTreeLayout.option_pyramid:
slice_size = 1
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients))
previous_slice = []
while len(tech_names) > slice_size:
slice = tech_names[:slice_size]
world.random.shuffle(slice)
tech_names = tech_names[slice_size:]
for i, tech_name in enumerate(previous_slice):
prerequisites.setdefault(slice[i], set()).add(tech_name)
prerequisites.setdefault(slice[i + 1], set()).add(tech_name)
previous_slice = slice
slice_size += 1
elif layout == TechTreeLayout.option_funnel:
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
# find largest inverse pyramid
# https://www.wolframalpha.com/input/?i=x+=+1/2+(n++++1)+(2++++n)+solve+for+n
import math
slice_size = int(0.5*(math.sqrt(8*len(tech_names)+1)-3))
tech_names.sort()
world.random.shuffle(tech_names)
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients))
previous_slice = []
while slice_size:
slice = tech_names[:slice_size]
world.random.shuffle(slice)
tech_names = tech_names[slice_size:]
if previous_slice:
for i, tech_name in enumerate(slice):
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
previous_slice = slice
slice_size -= 1
world.tech_tree_layout_prerequisites[player] = prerequisites
return prerequisites

View File

@@ -1,46 +1,154 @@
from __future__ import annotations
# Factorio technologies are imported from a .json document in /data
from typing import Dict
import os
from typing import Dict, Set, FrozenSet
import json
import Utils
import logging
factorio_id = 2**17
factorio_id = 2 ** 17
source_file = Utils.local_path("data", "factorio", "techs.json")
recipe_source_file = Utils.local_path("data", "factorio", "recipes.json")
with open(source_file) as f:
raw = json.load(f)
tech_table = {}
with open(recipe_source_file) as f:
raw_recipes = json.load(f)
tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {}
requirements = {}
ingredients = {}
all_ingredients = set()
always = lambda state: True
class Technology(): # maybe make subclass of Location?
def __init__(self, name, ingredients, factorio_id):
self.name = name
self.factorio_id = factorio_id
self.ingredients = ingredients
def build_rule(self, player: int):
logging.debug(f"Building rules for {self.name}")
ingredient_rules = []
for ingredient in self.ingredients:
logging.debug(f"Building rules for ingredient {ingredient}")
technologies = required_technologies[ingredient] # technologies that unlock the recipes
if technologies:
logging.debug(f"Required Technologies: {technologies}")
ingredient_rules.append(
lambda state, technologies=technologies: all(state.has(technology.name, player)
for technology in technologies))
if ingredient_rules:
ingredient_rules = frozenset(ingredient_rules)
return lambda state: all(rule(state) for rule in ingredient_rules)
return always
def get_prior_technologies(self, allowed_packs) -> Set[Technology]:
"""Get Technologies that have to precede this one to resolve tree connections."""
technologies = set()
for ingredient in self.ingredients:
if ingredient in allowed_packs:
technologies |= required_technologies[ingredient] # technologies that unlock the recipes
return technologies
def __hash__(self):
return self.factorio_id
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
return CustomTechnology(self, world, allowed_packs, player)
class CustomTechnology(Technology):
"""A particularly configured Technology for a world."""
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
ingredients = origin.ingredients & allowed_packs
self.player = player
if world.random_tech_ingredients[player]:
ingredients = list(ingredients)
ingredients.sort() # deterministic sample
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
class Recipe():
def __init__(self, name, category, ingredients, products):
self.name = name
self.category = category
self.ingredients = ingredients
self.products = products
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
@property
def unlocking_technologies(self) -> Set[Technology]:
"""Unlocked by any of the returned technologies. Empty set indicates a starting recipe."""
return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
# TODO: export this dynamically, or filter it during export
starting_ingredient_recipes = {"automation-science-pack"}
# recipes and technologies can share names in Factorio
for technology in sorted(raw):
data = raw[technology]
tech_table[technology] = factorio_id
for technology_name in sorted(raw):
data = raw[technology_name]
factorio_id += 1
if data["requires"]:
requirements[technology] = set(data["requires"])
current_ingredients = set(data["ingredients"])-starting_ingredient_recipes
if current_ingredients:
current_ingredients = set(data["ingredients"])
technology = Technology(technology_name, current_ingredients, factorio_id)
factorio_id += 1
tech_table[technology_name] = technology.factorio_id
technology_table[technology_name] = technology
all_ingredients |= current_ingredients
current_ingredients = {"recipe-"+ingredient for ingredient in current_ingredients}
ingredients[technology] = current_ingredients
recipe_sources = {}
recipe_sources: Dict[str, str] = {} # recipe_name -> technology source
for technology, data in raw.items():
recipe_source = all_ingredients & set(data["unlocks"])
for recipe in recipe_source:
recipe_sources["recipe-"+recipe] = technology
for recipe_name in data["unlocks"]:
recipe_sources.setdefault(recipe_name, set()).add(technology)
all_ingredients = {"recipe-"+ingredient for ingredient in all_ingredients}
del(raw)
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
del (raw)
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
all_product_sources: Dict[str, Set[Recipe]] = {}
for recipe_name, recipe_data in raw_recipes.items():
# example:
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))
if recipe.products != recipe.ingredients and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging
for product_name in recipe.products:
all_product_sources.setdefault(product_name, set()).add(recipe)
# build requirements graph for all technology ingredients
all_ingredient_names: Set[str] = set()
for technology in technology_table.values():
all_ingredient_names |= technology.ingredients
def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[Technology]:
if _done:
if ingredient_name in _done:
return set()
else:
_done.add(ingredient_name)
else:
_done = {ingredient_name}
recipes = all_product_sources.get(ingredient_name)
if not recipes:
return set()
current_technologies = set()
for recipe in recipes:
current_technologies |= recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
return current_technologies
required_technologies: Dict[str, FrozenSet[Technology]] = {}
for ingredient_name in all_ingredient_names:
required_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
advancement_technologies: Set[str] = set()
for technologies in required_technologies.values():
advancement_technologies |= {technology.name for technology in technologies}

View File

@@ -1,23 +1,22 @@
import logging
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from .Technologies import tech_table, requirements, ingredients, all_ingredients, recipe_sources
static_nodes = {"automation", "logistics"}
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies
from .Shapes import get_shapes
def gen_factorio(world: MultiWorld, player: int):
static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option?
for tech_name, tech_id in tech_table.items():
tech_item = Item(tech_name, True, tech_id, player)
tech_item = Item(tech_name, tech_name in advancement_technologies, tech_id, player)
tech_item.game = "Factorio"
if tech_name in static_nodes:
loc = world.get_location(tech_name, player)
loc.item = tech_item
loc.locked = loc.event = True
loc.locked = True
loc.event = tech_item.advancement
else:
world.itempool.append(tech_item)
set_rules(world, player)
world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player)
set_rules(world, player, custom_technologies)
def factorio_create_regions(world: MultiWorld, player: int):
@@ -30,30 +29,33 @@ def factorio_create_regions(world: MultiWorld, player: int):
tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech)
tech.game = "Factorio"
for ingredient in all_ingredients: # register science packs as events
ingredient_location = Location(player, ingredient, 0, nauvis)
ingredient_location.item = Item(ingredient, True, 0, player)
ingredient_location.event = ingredient_location.locked = True
menu.locations.append(ingredient_location)
crash.connect(nauvis)
world.regions += [menu, nauvis]
def set_custom_technologies(world: MultiWorld, player: int):
custom_technologies = {}
world_custom = getattr(world, "_custom_technologies", {})
world_custom[player] = custom_technologies
world._custom_technologies = world_custom
allowed_packs = world.max_science_pack[player].get_allowed_packs()
for technology_name, technology in technology_table.items():
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
return custom_technologies
def set_rules(world: MultiWorld, player: int):
def set_rules(world: MultiWorld, player: int, custom_technologies):
shapes = get_shapes(world, player)
if world.logic[player] != 'nologic':
from worlds.generic import Rules
for tech_name in tech_table:
# vanilla layout, to be implemented
# rules = requirements.get(tech_name, set()) | ingredients.get(tech_name, set())
# loose nodes
rules = ingredients.get(tech_name, set())
if rules:
location = world.get_location(tech_name, player)
Rules.set_rule(location, lambda state, rules=rules: all(state.has(rule, player) for rule in rules))
for recipe, technology in recipe_sources.items():
Rules.set_rule(world.get_location(recipe, player), lambda state, tech=technology: state.has(tech, player))
for tech_name, technology in custom_technologies.items():
location = world.get_location(tech_name, player)
Rules.set_rule(location, technology.build_rule(player))
prequisites = shapes.get(tech_name)
if prequisites:
locations = {world.get_location(requisite, player) for requisite in prequisites}
Rules.add_rule(location, lambda state,
locations=locations: all(state.can_reach(loc) for loc in locations))
world.completion_condition[player] = lambda state: all(state.has(ingredient, player)
for ingredient in all_ingredients)
# get all science pack technologies (but not the ability to craft them)
world.completion_condition[player] = lambda state: all(state.has(technology, player)
for technology in advancement_technologies)

View File

@@ -80,7 +80,7 @@ def gen_items(world: MultiWorld, player: int):
if item_data.type == "Event":
event_location = world.get_location(item_name, player)
world.push_item(event_location, item)
world.push_item(event_location, item, collect=False)
event_location.event = True
event_location.locked = True
if item.name == "King's_Pass":
@@ -94,13 +94,12 @@ def gen_items(world: MultiWorld, player: int):
world.push_item(event_location, item)
event_location.event = True
event_location.locked = True
world.push_precollected(item)
elif item_data.type == "Fake":
pass
elif item_data.type in not_shufflable_types:
location = world.get_location(item_name, player)
world.push_item(location, item)
world.push_item(location, item, collect=False)
location.event = item.advancement
location.locked = True
else:
@@ -110,7 +109,7 @@ def gen_items(world: MultiWorld, player: int):
pool.append(item)
else:
location = world.get_location(item_name, player)
world.push_item(location, item)
world.push_item(location, item, collect=False)
location.event = item.advancement
location.locked = True
logger.debug(f"Placed {item_name} to vanilla for player {player}")

74
worlds/minecraft/Items.py Normal file
View File

@@ -0,0 +1,74 @@
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
import typing
class ItemData(typing.NamedTuple):
code: int
progression: bool
class MinecraftItem(Item):
game: str = "Minecraft"
def __init__(self, name: str, progression: bool, code: int, player: int):
super().__init__(name, progression, code if code else None, player)
item_table = {
"Archery": ItemData(45000, True),
"Ingot Crafting": ItemData(45001, True),
"Resource Blocks": ItemData(45002, True),
"Brewing": ItemData(45003, True),
"Enchanting": ItemData(45004, True),
"Bucket": ItemData(45005, True),
"Flint and Steel": ItemData(45006, True),
"Bed": ItemData(45007, True),
"Bottles": ItemData(45008, True),
"Shield": ItemData(45009, True),
"Fishing Rod": ItemData(45010, True),
"Campfire": ItemData(45011, True),
"Progressive Weapons": ItemData(45012, True),
"Progressive Tools": ItemData(45013, True),
"Progressive Armor": ItemData(45014, True),
"8 Netherite Scrap": ItemData(45015, True),
"8 Emeralds": ItemData(45016, False),
"4 Emeralds": ItemData(45017, False),
"Channeling Book": ItemData(45018, True),
"Silk Touch Book": ItemData(45019, True),
"Sharpness III Book": ItemData(45020, False),
"Piercing IV Book": ItemData(45021, True),
"Looting III Book": ItemData(45022, False),
"Infinity Book": ItemData(45023, False),
"4 Diamond Ore": ItemData(45024, False),
"16 Iron Ore": ItemData(45025, False),
"500 XP": ItemData(45026, False),
"100 XP": ItemData(45027, False),
"50 XP": ItemData(45028, False),
"3 Ender Pearls": ItemData(45029, True),
"4 Lapis Lazuli": ItemData(45030, False),
"16 Porkchops": ItemData(45031, False),
"8 Gold Ore": ItemData(45032, False),
"Rotten Flesh": ItemData(45033, False),
"Single Arrow": ItemData(45034, False),
"Victory": ItemData(0, True)
}
# If not listed here then has frequency 1
item_frequencies = {
"Progressive Weapons": 3,
"Progressive Tools": 3,
"Progressive Armor": 2,
"8 Netherite Scrap": 2,
"8 Emeralds": 0,
"4 Emeralds": 8,
"4 Diamond Ore": 4,
"16 Iron Ore": 4,
"500 XP": 4, # 2 after exclusions
"100 XP": 10, # 4 after exclusions
"50 XP": 12, # 4 after exclusions
"3 Ender Pearls": 4,
"4 Lapis Lazuli": 2,
"16 Porkchops": 8,
"8 Gold Ore": 4,
"Rotten Flesh": 4,
"Single Arrow": 0
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}

View File

@@ -0,0 +1,142 @@
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
import typing
class AdvData(typing.NamedTuple):
id: int
region: str
class MinecraftAdvancement(Location):
game: str = "Minecraft"
def __init__(self, player: int, name: str, address: int, parent):
super().__init__(player, name, address if address else None, parent)
self.event = True if address == 0 else False
advancement_table = {
"Who is Cutting Onions?": AdvData(42000, 'Overworld'),
"Oh Shiny": AdvData(42001, 'Overworld'),
"Suit Up": AdvData(42002, 'Overworld'),
"Very Very Frightening": AdvData(42003, 'Village'),
"Hot Stuff": AdvData(42004, 'Overworld'),
"Free the End": AdvData(42005, 'The End'),
"A Furious Cocktail": AdvData(42006, 'Nether Fortress'),
"Best Friends Forever": AdvData(42007, 'Overworld'),
"Bring Home the Beacon": AdvData(42008, 'Nether Fortress'),
"Not Today, Thank You": AdvData(42009, 'Overworld'),
"Isn't It Iron Pick": AdvData(42010, 'Overworld'),
"Local Brewery": AdvData(42011, 'Nether Fortress'),
"The Next Generation": AdvData(42012, 'The End'),
"Fishy Business": AdvData(42013, 'Overworld'),
"Hot Tourist Destinations": AdvData(42014, 'The Nether'),
"This Boat Has Legs": AdvData(42015, 'The Nether'),
"Sniper Duel": AdvData(42016, 'Overworld'),
"Nether": AdvData(42017, 'The Nether'),
"Great View From Up Here": AdvData(42018, 'End City'),
"How Did We Get Here?": AdvData(42019, 'Nether Fortress'),
"Bullseye": AdvData(42020, 'Overworld'),
"Spooky Scary Skeleton": AdvData(42021, 'Nether Fortress'),
"Two by Two": AdvData(42022, 'The Nether'),
"Stone Age": AdvData(42023, 'Overworld'),
"Two Birds, One Arrow": AdvData(42024, 'Overworld'),
"We Need to Go Deeper": AdvData(42025, 'The Nether'),
"Who's the Pillager Now?": AdvData(42026, 'Pillager Outpost'),
"Getting an Upgrade": AdvData(42027, 'Overworld'),
"Tactical Fishing": AdvData(42028, 'Overworld'),
"Zombie Doctor": AdvData(42029, 'Overworld'),
"The City at the End of the Game": AdvData(42030, 'End City'),
"Ice Bucket Challenge": AdvData(42031, 'Overworld'),
"Remote Getaway": AdvData(42032, 'The End'),
"Into Fire": AdvData(42033, 'Nether Fortress'),
"War Pigs": AdvData(42034, 'Bastion Remnant'),
"Take Aim": AdvData(42035, 'Overworld'),
"Total Beelocation": AdvData(42036, 'Overworld'),
"Arbalistic": AdvData(42037, 'Overworld'),
"The End... Again...": AdvData(42038, 'The End'),
"Acquire Hardware": AdvData(42039, 'Overworld'),
"Not Quite \"Nine\" Lives": AdvData(42040, 'The Nether'),
"Cover Me With Diamonds": AdvData(42041, 'Overworld'),
"Sky's the Limit": AdvData(42042, 'End City'),
"Hired Help": AdvData(42043, 'Overworld'),
"Return to Sender": AdvData(42044, 'The Nether'),
"Sweet Dreams": AdvData(42045, 'Overworld'),
"You Need a Mint": AdvData(42046, 'The End'),
"Adventure": AdvData(42047, 'Overworld'),
"Monsters Hunted": AdvData(42048, 'Overworld'),
"Enchanter": AdvData(42049, 'Overworld'),
"Voluntary Exile": AdvData(42050, 'Pillager Outpost'),
"Eye Spy": AdvData(42051, 'Overworld'),
"The End": AdvData(42052, 'The End'),
"Serious Dedication": AdvData(42053, 'The Nether'),
"Postmortal": AdvData(42054, 'Village'),
"Monster Hunter": AdvData(42055, 'Overworld'),
"Adventuring Time": AdvData(42056, 'Overworld'),
"A Seedy Place": AdvData(42057, 'Overworld'),
"Those Were the Days": AdvData(42058, 'Bastion Remnant'),
"Hero of the Village": AdvData(42059, 'Village'),
"Hidden in the Depths": AdvData(42060, 'The Nether'),
"Beaconator": AdvData(42061, 'Nether Fortress'),
"Withering Heights": AdvData(42062, 'Nether Fortress'),
"A Balanced Diet": AdvData(42063, 'Village'),
"Subspace Bubble": AdvData(42064, 'The Nether'),
"Husbandry": AdvData(42065, 'Overworld'),
"Country Lode, Take Me Home": AdvData(42066, 'The Nether'),
"Bee Our Guest": AdvData(42067, 'Overworld'),
"What a Deal!": AdvData(42068, 'Village'),
"Uneasy Alliance": AdvData(42069, 'The Nether'),
"Diamonds!": AdvData(42070, 'Overworld'),
"A Terrible Fortress": AdvData(42071, 'Nether Fortress'),
"A Throwaway Joke": AdvData(42072, 'Overworld'),
"Minecraft": AdvData(42073, 'Overworld'),
"Sticky Situation": AdvData(42074, 'Overworld'),
"Ol' Betsy": AdvData(42075, 'Overworld'),
"Cover Me in Debris": AdvData(42076, 'The Nether'),
"The End?": AdvData(42077, 'The End'),
"The Parrots and the Bats": AdvData(42078, 'Overworld'),
"A Complete Catalogue": AdvData(42079, 'Village'),
"Getting Wood": AdvData(42080, 'Overworld'),
"Time to Mine!": AdvData(42081, 'Overworld'),
"Hot Topic": AdvData(42082, 'Overworld'),
"Bake Bread": AdvData(42083, 'Overworld'),
"The Lie": AdvData(42084, 'Overworld'),
"On a Rail": AdvData(42085, 'Overworld'),
"Time to Strike!": AdvData(42086, 'Overworld'),
"Cow Tipper": AdvData(42087, 'Overworld'),
"When Pigs Fly": AdvData(42088, 'Overworld'),
"Overkill": AdvData(42089, 'Nether Fortress'),
"Librarian": AdvData(42090, 'Overworld'),
"Overpowered": AdvData(42091, 'Overworld'),
"Ender Dragon": AdvData(0, 'The End')
}
exclusion_table = {
"hard": {
"Very Very Frightening": "50 XP",
"Two by Two": "100 XP",
"Two Birds, One Arrow": "50 XP",
"Arbalistic": "100 XP",
"Beaconator": "50 XP",
"A Balanced Diet": "100 XP",
"Uneasy Alliance": "100 XP",
"Cover Me in Debris": "100 XP",
"A Complete Catalogue": "50 XP",
"Overpowered": "50 XP"
},
"insane": {
"How Did We Get Here?": "500 XP",
"Adventuring Time": "500 XP"
},
"postgame": {
"The Next Generation": "50 XP",
"The End... Again...": "50 XP",
"You Need a Mint": "50 XP",
"Monsters Hunted": "100 XP"
}
}
events_table = {
"Ender Dragon": "Victory"
}
lookup_id_to_name: typing.Dict[int, str] = {loc_data.id: loc_name for loc_name, loc_data in advancement_table.items() if loc_data.id}

101
worlds/minecraft/Regions.py Normal file
View File

@@ -0,0 +1,101 @@
from .Locations import MinecraftAdvancement, advancement_table
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
def minecraft_create_regions(world: MultiWorld, player: int):
def MCRegion(region_name: str, exits=[]):
ret = Region(region_name, None, region_name, player)
ret.world = world
ret.locations = [ MinecraftAdvancement(player, loc_name, loc_data.id, ret)
for loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name ]
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret
world.regions += [MCRegion(*r) for r in mc_regions]
for (exit, region) in mandatory_connections:
world.get_entrance(exit, player).connect(world.get_region(region, player))
def link_minecraft_structures(world: MultiWorld, player: int):
# Get all unpaired exits and all regions without entrances (except the Menu)
# This function is destructive on these lists.
exits = [exit.name for r in world.regions if r.player == player for exit in r.exits if exit.connected_region == None]
structs = [r.name for r in world.regions if r.player == player and r.entrances == [] and r.name != 'Menu']
try:
assert len(exits) == len(structs)
except AssertionError as e: # this should never happen
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player}") from e
num_regions = len(exits)
pairs = {}
def check_valid_connection(exit, struct):
if (exit in exits) and (struct in structs) and (exit not in pairs):
return True
return False
def set_pair(exit, struct):
pairs[exit] = struct
exits.remove(exit)
structs.remove(struct)
# Plando stuff. Remove any utilized exits/structs from the lists.
# Raise error if trying to put Nether Fortress in the End.
if world.shuffle_structures[player]:
# Can't put Nether Fortress in the End
if 'The End Structure' in exits and 'Nether Fortress' in structs:
try:
end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress'])
set_pair('The End Structure', end_struct)
except IndexError as e:
raise Exception(f"Plando forced Nether Fortress in the End for player {player}") from e
world.random.shuffle(structs)
for exit, struct in zip(exits[:], structs[:]):
set_pair(exit, struct)
else: # write remaining default connections
for (exit, struct) in default_connections:
if exit in exits:
set_pair(exit, struct)
# Make sure we actually paired everything; might fail if plando
try:
assert len(exits) == len(structs) == 0
except AssertionError as e:
raise Exception(f"Failed to connect all Minecraft structures for player {player}; check plando settings in yaml") from e
for exit, struct in pairs.items():
world.get_entrance(exit, player).connect(world.get_region(struct, player))
if world.shuffle_structures[player]:
world.spoiler.set_entrance(exit, struct, 'entrance', player)
# (Region name, list of exits)
mc_regions = [
('Menu', ['New World']),
('Overworld', ['Nether Portal', 'End Portal', 'Overworld Structure 1', 'Overworld Structure 2']),
('The Nether', ['Nether Structure 1', 'Nether Structure 2']),
('The End', ['The End Structure']),
('Village', []),
('Pillager Outpost', []),
('Nether Fortress', []),
('Bastion Remnant', []),
('End City', [])
]
# (Entrance, region pointed to)
mandatory_connections = [
('New World', 'Overworld'),
('Nether Portal', 'The Nether'),
('End Portal', 'The End')
]
default_connections = {
('Overworld Structure 1', 'Village'),
('Overworld Structure 2', 'Pillager Outpost'),
('Nether Structure 1', 'Nether Fortress'),
('Nether Structure 2', 'Bastion Remnant'),
('The End Structure', 'End City')
}

143
worlds/minecraft/Rules.py Normal file
View File

@@ -0,0 +1,143 @@
from ..generic.Rules import set_rule
from .Locations import exclusion_table, events_table
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from Options import AdvancementGoal
def set_rules(world: MultiWorld, player: int):
def reachable_locations(state):
postgame_advancements = set(exclusion_table['postgame'].keys())
postgame_advancements.add('Free the End')
for event in events_table.keys():
postgame_advancements.add(event)
return [location for location in world.get_locations() if
(player is None or location.player == player) and
(location.name not in postgame_advancements) and
location.can_reach(state)]
# 92 total advancements, 16 are typically excluded, 1 is Free the End. Goal is to complete X advancements and then Free the End.
goal_map = {
'few': 30,
'normal': 50,
'many': 70
}
goal = goal_map[getattr(world, 'advancement_goal')[player].get_option_name()]
can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', player) and state.can_kill_ender_dragon(player)
if world.logic[player] != 'nologic':
world.completion_condition[player] = lambda state: state.has('Victory', player)
set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and
(state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and
state.has_iron_ingots(player))
set_rule(world.get_entrance("End Portal", player), lambda state: state.enter_stronghold(player) and state.has('3 Ender Pearls', player, 4))
set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state.can_adventure(player))
set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state.can_adventure(player))
set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state.can_adventure(player))
set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state.can_adventure(player))
set_rule(world.get_entrance("The End Structure", player), lambda state: state.can_adventure(player))
set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state))
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state.can_piglin_trade(player))
set_rule(world.get_location("Oh Shiny", player), lambda state: state.can_piglin_trade(player))
set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant(player))
set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Free the End", player), lambda state: can_complete(state))
set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and state.has("Fishing Rod", player) and state.can_reach('The Nether', 'Region', player))
set_rule(world.get_location("Best Friends Forever", player), lambda state: True)
set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and
state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player))
set_rule(world.get_location("Local Brewery", player), lambda state: state.can_brew_potions(player))
set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state))
set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player))
set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player))
set_rule(world.get_location("This Boat Has Legs", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player))
set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player))
set_rule(world.get_location("Nether", player), lambda state: True)
set_rule(world.get_location("Great View From Up Here", player), lambda state: state.basic_combat(player))
set_rule(world.get_location("How Did We Get Here?", player), lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player) and # most effects; Absorption
state.can_reach('End City', 'Region', player) and state.can_reach('The Nether', 'Region', player) and # Levitation; potion ingredients
state.has("Fishing Rod", player) and state.has("Archery", player) and # Pufferfish, Nautilus Shells; spectral arrows
state.can_reach("Bring Home the Beacon", "Location", player) and # Haste
state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village
set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player))
set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state.basic_combat(player))
set_rule(world.get_location("Two by Two", player), lambda state: state.has_iron_ingots(player) and state.can_adventure(player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots
set_rule(world.get_location("Stone Age", player), lambda state: True)
set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state.craft_crossbow(player) and state.can_enchant(player))
set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True)
set_rule(world.get_location("Who's the Pillager Now?", player), lambda state: state.craft_crossbow(player))
set_rule(world.get_location("Getting an Upgrade", player), lambda state: state.has("Progressive Tools", player))
set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Zombie Doctor", player), lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player))
set_rule(world.get_location("The City at the End of the Game", player), lambda state: True)
set_rule(world.get_location("Ice Bucket Challenge", player), lambda state: state.has_diamond_pickaxe(player))
set_rule(world.get_location("Remote Getaway", player), lambda state: True)
set_rule(world.get_location("Into Fire", player), lambda state: state.basic_combat(player))
set_rule(world.get_location("War Pigs", player), lambda state: state.basic_combat(player))
set_rule(world.get_location("Take Aim", player), lambda state: state.has("Archery", player))
set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state.can_use_anvil(player) and state.can_enchant(player))
set_rule(world.get_location("Arbalistic", player), lambda state: state.craft_crossbow(player) and state.has("Piercing IV Book", player) and
state.can_use_anvil(player) and state.can_enchant(player))
set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state) and state.has("Ingot Crafting", player) and state.can_reach('The Nether', 'Region', player)) # furnace for glass, nether for ghast tears
set_rule(world.get_location("Acquire Hardware", player), lambda state: state.has_iron_ingots(player))
set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state.can_piglin_trade(player) and state.has("Resource Blocks", player))
set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player))
set_rule(world.get_location("Sky's the Limit", player), lambda state: state.basic_combat(player))
set_rule(world.get_location("Hired Help", player), lambda state: state.has("Resource Blocks", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Return to Sender", player), lambda state: True)
set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player))
set_rule(world.get_location("You Need a Mint", player), lambda state: can_complete(state) and state.has_bottle_mc(player))
set_rule(world.get_location("Adventure", player), lambda state: True)
set_rule(world.get_location("Monsters Hunted", player), lambda state: can_complete(state) and state.can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing
set_rule(world.get_location("Enchanter", player), lambda state: state.can_enchant(player))
set_rule(world.get_location("Voluntary Exile", player), lambda state: state.basic_combat(player))
set_rule(world.get_location("Eye Spy", player), lambda state: state.enter_stronghold(player))
set_rule(world.get_location("The End", player), lambda state: True)
set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player))
set_rule(world.get_location("Postmortal", player), lambda state: state.complete_raid(player))
set_rule(world.get_location("Monster Hunter", player), lambda state: True)
set_rule(world.get_location("Adventuring Time", player), lambda state: state.can_adventure(player))
set_rule(world.get_location("A Seedy Place", player), lambda state: True)
set_rule(world.get_location("Those Were the Days", player), lambda state: True)
set_rule(world.get_location("Hero of the Village", player), lambda state: state.complete_raid(player))
set_rule(world.get_location("Hidden in the Depths", player), lambda state: state.can_brew_potions(player) and state.has("Bed", player) and state.has_diamond_pickaxe(player)) # bed mining :)
set_rule(world.get_location("Beaconator", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and
state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
set_rule(world.get_location("Withering Heights", player), lambda state: state.can_kill_wither(player))
set_rule(world.get_location("A Balanced Diet", player), lambda state: state.has_bottle_mc(player) and state.has_gold_ingots(player) and # honey bottle; gapple
state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit
set_rule(world.get_location("Subspace Bubble", player), lambda state: state.has_diamond_pickaxe(player))
set_rule(world.get_location("Husbandry", player), lambda state: True)
set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player))
set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player))
set_rule(world.get_location("What a Deal!", player), lambda state: True)
set_rule(world.get_location("Uneasy Alliance", player), lambda state: state.has_diamond_pickaxe(player))
set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player))
set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything
set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned
set_rule(world.get_location("Minecraft", player), lambda state: True)
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has_bottle_mc(player))
set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(player))
set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and
state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and
state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player))
set_rule(world.get_location("The End?", player), lambda state: True)
set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True)
set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw
set_rule(world.get_location("Getting Wood", player), lambda state: True)
set_rule(world.get_location("Time to Mine!", player), lambda state: True)
set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Ingot Crafting", player))
set_rule(world.get_location("Bake Bread", player), lambda state: True)
set_rule(world.get_location("The Lie", player), lambda state: state.has_iron_ingots(player) and state.has("Bucket", player))
set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player))
set_rule(world.get_location("Time to Strike!", player), lambda state: True)
set_rule(world.get_location("Cow Tipper", player), lambda state: True)
set_rule(world.get_location("When Pigs Fly", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player) and state.can_adventure(player)) # saddles in fortress chests
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and state.has("Progressive Weapons", player)) # strength 1, stone axe crit
set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player))
set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player))

View File

@@ -0,0 +1,68 @@
from random import Random
from .Items import MinecraftItem, item_table, item_frequencies
from .Locations import exclusion_table, events_table
from .Regions import link_minecraft_structures
from .Rules import set_rules
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from Options import minecraft_options
client_version = (0, 3)
def generate_mc_data(world: MultiWorld, player: int, seedname: str):
import base64, json
from Utils import output_path
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", "The End Structure"]
data = {
'world_seed': Random(world.rom_seeds[player]).getrandbits(32), # consistent and doesn't interfere with other generation
'seed_name': seedname,
'player_name': world.get_player_names(player),
'client_version': client_version,
'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits}
}
filename = f"AP_{seedname}_P{player}_{world.get_player_names(player)}.apmc"
with open(output_path(filename), 'wb') as f:
f.write(base64.b64encode(bytes(json.dumps(data), 'utf-8')))
def fill_minecraft_slot_data(world: MultiWorld, player: int):
slot_data = {}
for option_name in minecraft_options:
option = getattr(world, option_name)[player]
slot_data[option_name] = int(option.value)
slot_data['client_version'] = client_version
return slot_data
# Generates the item pool given the table and frequencies in Items.py.
def minecraft_gen_item_pool(world: MultiWorld, player: int):
pool = []
for item_name, item_data in item_table.items():
for count in range(item_frequencies.get(item_name, 1)):
pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, player))
prefill_pool = {}
prefill_pool.update(events_table)
exclusion_pools = ['hard', 'insane', 'postgame']
for key in exclusion_pools:
if not getattr(world, f"include_{key}_advancements")[player]:
prefill_pool.update(exclusion_table[key])
for loc_name, item_name in prefill_pool.items():
item_data = item_table[item_name]
location = world.get_location(loc_name, player)
item = MinecraftItem(item_name, item_data.progression, item_data.code, player)
world.push_item(location, item, collect=False)
pool.remove(item)
location.event = item_data.progression
location.locked = True
world.itempool += pool
# Generate Minecraft world.
def gen_minecraft(world: MultiWorld, player: int):
link_minecraft_structures(world, player)
minecraft_gen_item_pool(world, player)
set_rules(world, player)