Compare commits

..

79 Commits
0.1.3 ... 0.1.4

Author SHA1 Message Date
Fabian Dill
babd809fa6 Factorio: fix certain recipes (like steel-plate) not getting their crafting time adjusted correctly. 2021-07-10 08:01:39 +02:00
Fabian Dill
54177c7064 bump required LttP Client Version 2021-07-10 07:37:56 +02:00
Fabian Dill
4884184e4a fix autolauncher import 2021-07-09 22:47:35 +02:00
Fabian Dill
4c7ef593be Some optimizations 2021-07-09 17:44:24 +02:00
Fabian Dill
2600e9a805 Factorio: add coal liquefaction and kovarex process to progressive processing 2021-07-09 04:49:19 +02:00
Fabian Dill
6ac74f5686 Mystery: mention failing option name 2021-07-09 03:06:16 +02:00
Fabian Dill
172c1789a8 introduce World.topology_present, to indicate if any meaningful path information is available in the world 2021-07-08 11:07:41 +02:00
Fabian Dill
ffc00b7800 Factorio: fix progressive science pack order 2021-07-08 05:09:34 +02:00
Fabian Dill
f44f015cb9 typo in playerSettings.yaml 2021-07-08 00:02:17 +02:00
Fabian Dill
a4dcda16c1 LttP: update SNI handling to v34 2021-07-07 22:53:01 +02:00
Fabian Dill
9db506ef42 Factorio: recipe randomization (rocket-part and science-packs only for now) 2021-07-07 10:14:58 +02:00
Fabian Dill
007f2caecf LttP: SNI keeps its log outside its folder 2021-07-07 03:49:29 +02:00
Fabian Dill
80a5845695 LttP: Move over to SNI 2021-07-07 03:45:27 +02:00
Fabian Dill
1b5525a8c5 Factorio: fix uranium-ore recipe writing to mod 2021-07-06 23:55:30 +02:00
Fabian Dill
22d45b9571 Factorio: remove loaders from recipe list 2021-07-06 13:15:03 +02:00
Fabian Dill
773602169d Factorio: fix some form mistakes that didn't break anything (yet) 2021-07-06 13:06:45 +02:00
Fabian Dill
b650d3d9e6 Factorio: include recipe amounts in Recipe data 2021-07-06 12:35:27 +02:00
Fabian Dill
9b2171088e Factorio: mark all potential rocket recipe ingredients as advancements 2021-07-06 12:33:33 +02:00
Fabian Dill
e58ae58e24 Factorio: add Progressive Option 2021-07-04 22:21:53 +02:00
Fabian Dill
a11e840d36 Cache some MultiWorld properties 2021-07-04 16:44:27 +02:00
Fabian Dill
7d5b20ccfc Remove temporary solution "OptionSets" in favor of AutoWorld's Options 2021-07-04 16:18:21 +02:00
Fabian Dill
2530d28c9d Move Progressive Items to AutoWorld 2021-07-04 15:47:11 +02:00
Fabian Dill
c669bc3e7f Factorio: correctly display player names with spaces and detect desyncs 2021-07-04 15:25:56 +02:00
espeon65536
5943c8975a fixing the tests for bees again 2021-07-03 01:55:47 +00:00
espeon65536
d9f97f6aad Improve option retrieval to fix test crashing 2021-07-03 01:55:47 +00:00
espeon65536
576521229c Added option for MC bee traps 2021-07-03 01:55:47 +00:00
Fabian Dill
ac919f72a8 Factorio: update setup 2021-07-03 00:30:00 +02:00
Fabian Dill
85ce2aff47 Factorio: RIP Bridge File 2021-07-02 20:52:06 +02:00
Fabian Dill
8030db03ad Merge remote-tracking branch 'Espeon/minecraft' into Archipelago_Main 2021-07-02 20:14:34 +02:00
espeon65536
1e90470862 increment MC client version and network_data_package version 2021-07-02 10:12:06 -05:00
espeon65536
e37ca97bde add bee traps 2021-07-02 10:10:35 -05:00
Fabian Dill
97f45f5d96 FactorioClient:
fix reconnect
add auto-world-gen

todo:
move remaining script output bridge to rcon
2021-07-02 01:58:03 +02:00
Fabian Dill
0a64caf4c5 add Factorio world gen settings 2021-07-02 01:29:49 +02:00
Fabian Dill
eee6fc0f10 increment version 2021-07-01 21:18:08 +02:00
Fabian Dill
60972e026b send packed NetworkItem in PrintJSON 2021-06-30 20:57:00 +02:00
Fabian Dill
fd9123610b mimic ItemSend fields of PrintJSON for hints 2021-06-30 20:45:06 +02:00
alwaysintreble
6458653812 Update Text.py 2021-06-29 22:00:06 +00:00
Fabian Dill
328d448ab2 Auto import worlds to trigger registration 2021-06-29 03:49:29 +02:00
Fabian Dill
10aca70879 update Flask-Compress 2021-06-29 03:27:31 +02:00
Fabian Dill
92edc68890 update prompt_toolkit 2021-06-29 03:26:16 +02:00
Fabian Dill
4d4af9d74e WebHost: Guard each Room via file-lock 2021-06-29 03:11:48 +02:00
espeon65536
92c21de61d Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into minecraft 2021-06-28 14:45:20 -05:00
espeon65536
f918d34098 un-disabled villages spawning in nether 2021-06-28 14:41:33 -05:00
Fabian Dill
95e0f551e8 LttP Client: restore auto-snes 2021-06-28 01:27:52 +02:00
espeon65536
43e17f82b0 Updated HK test to use autoworld 2021-06-27 23:26:24 +00:00
espeon65536
c7417623e6 Converted Hollow Knight to AutoWorld system 2021-06-27 23:26:24 +00:00
black-sliver
50ed657b0e Allow running MultiMystery.py from source on linux 2021-06-27 18:00:36 +00:00
Fabian Dill
8b5d7028f7 decrement Factorio Client version
(for now, as nobody has that client yet)
2021-06-27 05:18:44 +02:00
Chris Wilson
aa28b3887f Apply Dewin's suggested filter to the Z3 Player Tracker 2021-06-26 22:32:29 -04:00
Fabian Dill
739b563bc2 Move required Client Version to AutoWorld 2021-06-27 00:23:42 +02:00
Fabian Dill
a3a68de341 Factorio: only create events for required technologies 2021-06-26 06:05:38 +02:00
espeon65536
57c761aa7d Made AdvancementGoal a Range again
also fixed the awful rules formatting
2021-06-25 20:15:07 -05:00
espeon65536
75891b2d38 fix tests again 2021-06-25 19:59:44 -05:00
espeon65536
44943f6bf8 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into minecraft 2021-06-25 19:44:15 -05:00
Fabian Dill
5fdcd2d7c7 Factorio: locale formatting fixes 2021-06-26 00:54:27 +02:00
Fabian Dill
43e3c84635 fix the Hollow Knight Unittest. Yes, the one test. 2021-06-25 23:39:22 +02:00
Fabian Dill
7f8bb10fc5 Move Factorio, Hollow Knight and Minecraft Options into AutoWorld 2021-06-25 23:32:13 +02:00
Chris Wilson
cc85edafc4 Add "Host Game" button back to the website landing page 2021-06-25 16:59:59 -04:00
Fabian Dill
878ab33039 Factorio: fix incomplete crafting category copy 2021-06-25 22:09:09 +02:00
Fabian Dill
4b495557cd Tracker: sort numbers and fractions numerically 2021-06-25 21:15:54 +02:00
Fabian Dill
d1fd1cd788 Tracker: sort Last Activity numerically, instead of text. 2021-06-25 21:05:44 +02:00
Fabian Dill
f870bb3fad MultiServer:
implement a hint recheck that triggers on get_save()
Still torn if I want a single hint list per team and filter on demand, or have filtered lists and re_check on demand.
2021-06-25 21:04:37 +02:00
espeon65536
719f9d7d48 Monsters Hunted made a hard-postgame advancement, so both flags must be set for it to be not junkfilled 2021-06-25 13:57:09 -05:00
espeon65536
fd811bfd1b fix minecraft tests 2021-06-25 13:02:45 -05:00
espeon65536
6837cd2917 Require the ability to respawn the dragon for all dragon-related advancements 2021-06-25 12:43:59 -05:00
espeon65536
f778a263a7 Forbid villages from spawning in the Nether 2021-06-25 12:37:06 -05:00
Fabian Dill
007f66d86e CommonClient.py: fix generic error 2021-06-25 07:25:03 +02:00
Fabian Dill
0e32393acb FactorioClient: only await awaitable tasks 2021-06-25 07:11:06 +02:00
Fabian Dill
20729242f9 allow nested dictionaries in dict_to_lua 2021-06-25 01:55:58 +02:00
Fabian Dill
91655a855d Factorio: exclude science packs and rocket-part from free samples 2021-06-25 01:31:48 +02:00
Fabian Dill
9f2f343f76 Factorio: always display static nodes with full info 2021-06-24 23:51:42 +02:00
espeon65536
16ae77ca1c Plandoing structures causes them to output in the spoiler log 2021-06-16 20:24:36 -05:00
espeon65536
cd0306d513 additional import cleanup 2021-06-16 01:16:19 -05:00
espeon65536
b29d0b8276 Fixed some options in the Minecraft section of playerSettings 2021-06-15 22:27:51 -05:00
espeon65536
e49d10ab22 Clean up imports 2021-06-15 18:22:12 -05:00
espeon65536
059946d59e Shifted Minecraft to the new AutoWorld system 2021-06-15 18:15:05 -05:00
espeon65536
6211760922 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into minecraft 2021-06-15 16:58:28 -05:00
espeon65536
d7a46f089e added get_option_name to Range option for spoiler generation 2021-06-08 08:59:06 -05:00
espeon65536
6e33181f05 Changed advancement_goal to a Range option 2021-06-08 08:58:16 -05:00
62 changed files with 1604 additions and 1247 deletions

View File

@@ -142,38 +142,37 @@ class MultiWorld():
def set_options(self, args):
import Options
from worlds import AutoWorld
for option_set in Options.option_sets:
for option in option_set:
setattr(self, option, getattr(args, option, {}))
for player in self.player_ids:
self.custom_data[player] = {}
self.worlds[player] = AutoWorld.AutoWorldRegister.world_types[self.game[player]](self, player)
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option in world_type.options:
setattr(self, option, getattr(args, option, {}))
self.worlds[player] = world_type(self, player)
def secure(self):
self.random = secrets.SystemRandom()
@property
@functools.cached_property
def player_ids(self):
yield from range(1, self.players + 1)
return tuple(range(1, self.players + 1))
@property
# Todo: make these automatic, or something like get_players_for_game(game_name)
@functools.cached_property
def alttp_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
@property
@functools.cached_property
def hk_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
@property
@functools.cached_property
def factorio_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
@property
@functools.cached_property
def minecraft_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
return tuple(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)})'
@@ -238,53 +237,12 @@ class MultiWorld():
def get_all_state(self, keys=False) -> CollectionState:
ret = CollectionState(self)
def soft_collect(item):
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
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[
item.player].progressive_sword_limit >= 4:
ret.prog_items['Golden Sword', item.player] += 1
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[
item.player].progressive_sword_limit >= 3:
ret.prog_items['Tempered Sword', item.player] += 1
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
ret.prog_items['Master Sword', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
ret.prog_items['Fighter Sword', item.player] += 1
elif 'Glove' in item.name:
if ret.has('Titans Mitts', item.player):
pass
elif ret.has('Power Glove', item.player):
ret.prog_items['Titans Mitts', item.player] += 1
else:
ret.prog_items['Power Glove', item.player] += 1
elif 'Shield' in item.name:
if ret.has('Mirror Shield', item.player):
pass
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
ret.prog_items['Mirror Shield', item.player] += 1
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
ret.prog_items['Red Shield', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
ret.prog_items['Blue Shield', item.player] += 1
elif 'Bow' in item.name:
if ret.has('Silver', item.player):
pass
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
ret.prog_items['Silver Bow', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
ret.prog_items['Bow', item.player] += 1
elif item.advancement or item.smallkey or item.bigkey:
ret.prog_items[item.name, item.player] += 1
for item in self.itempool:
soft_collect(item)
self.worlds[item.player].collect(ret, item)
if keys:
for p in self.alttp_player_ids:
world = self.worlds[p]
from worlds.alttp.Items import ItemFactory
for item in ItemFactory(
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
@@ -299,7 +257,7 @@ class MultiWorld():
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
'Small Key (Ganons Tower)'] * 4,
p):
soft_collect(item)
world.collect(ret, item)
ret.sweep_for_events()
return ret
@@ -891,72 +849,24 @@ class CollectionState(object):
return self.fortress_loot(player) and normal_kill
def can_kill_ender_dragon(self, player: int):
# Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned.
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Ingot Crafting', player)
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)
return respawn_dragon and 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)
return respawn_dragon and ((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 respawn_dragon and 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)
changed = False
# TODO: create a mapping for progressive items in each game and use that
if item.game == "A Link to the Past":
if item.name.startswith('Progressive '):
if 'Sword' in item.name:
if self.has('Golden Sword', item.player):
pass
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
item.player].progressive_sword_limit >= 4:
self.prog_items['Golden Sword', item.player] += 1
changed = True
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
self.prog_items['Tempered Sword', item.player] += 1
changed = True
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
self.prog_items['Master Sword', item.player] += 1
changed = True
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
self.prog_items['Fighter Sword', item.player] += 1
changed = True
elif 'Glove' in item.name:
if self.has('Titans Mitts', item.player):
pass
elif self.has('Power Glove', item.player):
self.prog_items['Titans Mitts', item.player] += 1
changed = True
else:
self.prog_items['Power Glove', item.player] += 1
changed = True
elif 'Shield' in item.name:
if self.has('Mirror Shield', item.player):
pass
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
self.prog_items['Mirror Shield', item.player] += 1
changed = True
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
self.prog_items['Red Shield', item.player] += 1
changed = True
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
self.prog_items['Blue Shield', item.player] += 1
changed = True
elif 'Bow' in item.name:
if self.has('Silver Bow', item.player):
pass
elif self.has('Bow', item.player):
self.prog_items['Silver Bow', item.player] += 1
changed = True
else:
self.prog_items['Bow', item.player] += 1
changed = True
changed = self.world.worlds[item.player].collect(self, item)
if not changed and (event or item.advancement):
if not changed and event:
self.prog_items[item.name, item.player] += 1
changed = True
@@ -1426,8 +1336,6 @@ class Spoiler(object):
'shuffle': self.world.shuffle,
'item_pool': self.world.difficulty,
'item_functionality': self.world.item_functionality,
'gt_crystals': self.world.crystals_needed_for_gt,
'ganon_crystals': self.world.crystals_needed_for_ganon,
'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility,
'hints': self.world.hints,
@@ -1451,7 +1359,6 @@ class Spoiler(object):
'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required,
'shop_shuffle': self.world.shop_shuffle,
'shop_item_slots': self.world.shop_item_slots,
'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
@@ -1501,22 +1408,13 @@ class Spoiler(object):
outfile.write('Progression Balanced: %s\n' % (
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
if player in self.world.hk_player_ids:
for hk_option in Options.hollow_knight_options:
res = getattr(self.world, hk_option)[player]
outfile.write(f'{hk_option+":":33}{res}\n')
elif player in self.world.factorio_player_ids:
for f_option in Options.factorio_options:
options = self.world.worlds[player].options
if options:
for f_option in options:
res = getattr(self.world, f_option)[player]
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
elif player in self.world.minecraft_player_ids:
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')
elif player in self.world.alttp_player_ids:
if player in self.world.alttp_player_ids:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
@@ -1544,8 +1442,6 @@ class Spoiler(object):
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
@@ -1568,8 +1464,6 @@ class Spoiler(object):
"f" in self.metadata["shop_shuffle"][player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop Item Slots: %s\n' %
self.metadata["shop_item_slots"][player])
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
@@ -1598,6 +1492,13 @@ class Spoiler(object):
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.world.factorio_player_ids:
outfile.write('\n\nRecipes:\n')
for player in self.world.factorio_player_ids:
name = self.world.get_player_names(player)
for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))

View File

@@ -4,9 +4,7 @@ import typing
import asyncio
import urllib.parse
import prompt_toolkit
import websockets
from prompt_toolkit.patch_stdout import patch_stdout
import Utils
from MultiServer import CommandProcessor
@@ -210,8 +208,6 @@ class CommonContext():
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"]))
@@ -331,9 +327,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.error('Invalid password')
ctx.password = None
await ctx.server_auth(True)
else:
elif errors:
raise Exception("Unknown connection errors: " + str(errors))
raise Exception('Connection refused by the multiworld host, no reason provided')
else:
raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected':
ctx.team = args["team"]

View File

@@ -5,7 +5,8 @@ import json
import string
import copy
import sys
from concurrent.futures import ThreadPoolExecutor
import subprocess
import factorio_rcon
import colorama
import asyncio
@@ -21,9 +22,9 @@ from worlds.factorio.Technologies import lookup_id_to_name
rcon_port = 24242
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
save_name = "Archipelago"
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
bin_dir = os.path.dirname(executable)
@@ -35,9 +36,7 @@ if not os.path.exists(executable):
else:
raise FileNotFoundError(executable)
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
thread_pool = ThreadPoolExecutor(10)
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
class FactorioCommandProcessor(ClientCommandProcessor):
@@ -56,7 +55,10 @@ class FactorioCommandProcessor(ClientCommandProcessor):
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.")
if self.ctx.rcon_client:
get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code
else:
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
return super(FactorioCommandProcessor, self)._cmd_connect(address)
@@ -99,54 +101,49 @@ class FactorioContext(CommonContext):
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}.zip"
async def game_watcher(ctx: FactorioContext, bridge_file: str):
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0
try:
while not ctx.exit_event.is_set():
if os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.")
while not ctx.exit_event.is_set():
if ctx.awaiting_bridge:
ctx.awaiting_bridge = False
with open(bridge_file) as f:
data = json.load(f)
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"]
ctx.seed_name = data["seed_name"]
if ctx.awaiting_bridge and ctx.rcon_client:
ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if data["slot_name"] != ctx.auth:
logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
elif data["seed_name"] != ctx.seed_name:
logger.warning(f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
else:
data = data["info"]
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
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:
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)
if ctx.locations_checked != research_data:
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:
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 stream_factorio_output(pipe, queue, process):
def queuer():
while 1:
while process.poll() is None:
text = pipe.readline().strip()
if text:
queue.put_nowait(text)
@@ -155,25 +152,32 @@ def stream_factorio_output(pipe, queue):
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
thread.start()
return thread
async def factorio_server_watcher(ctx: FactorioContext):
import subprocess
import factorio_rcon
factorio_server_logger = logging.getLogger("FactorioServer")
factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)),
savegame_name = os.path.abspath(ctx.savegame_name)
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name
))
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
*(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue)
stream_factorio_output(factorio_process.stderr, factorio_queue)
script_folder = None
progression_watcher = None
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
try:
while not ctx.exit_event.is_set():
if factorio_process.poll():
factorio_server_logger.info("Factorio server has exited.")
ctx.exit_event.set()
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
@@ -182,16 +186,7 @@ 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}")
progression_watcher = asyncio.create_task(
game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
ctx.awaiting_bridge = True
if ctx.rcon_client:
while ctx.send_index < len(ctx.items_received):
@@ -203,47 +198,104 @@ async def factorio_server_watcher(ctx: FactorioContext):
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.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
ctx.send_index += 1
await asyncio.sleep(1)
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.rcon_client = None
ctx.exit_event.set()
finally:
factorio_process.terminate()
if progression_watcher:
await progression_watcher
async def main():
def get_info(ctx, rcon_client):
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
async def factorio_spinup_server(ctx: FactorioContext):
savegame_name = os.path.abspath("Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name, "--preset", "archipelago"
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Information Exchange Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
rcon_client = None
try:
while not ctx.auth:
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.exit_event.set()
else:
logger.info(f"Got World Information from AP Mod for seed {ctx.seed_name} in slot {ctx.auth}")
finally:
factorio_process.terminate()
async def main(ui=None):
ctx = FactorioContext(None, None, True)
# testing shortcuts
# ctx.server_address = "localhost"
# ctx.auth = "Nauvis"
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
await asyncio.sleep(3)
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if ui:
input_task = None
ui_app = ui(ctx)
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
await factorio_server_task
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
ctx.snes_reconnect_address = None
await asyncio.gather(input_task, factorio_server_task)
await progression_watcher
await factorio_server_task
if ctx.server is not None and not ctx.server.socket.closed:
if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task is not None:
await ctx.server_task
await factorio_server_task
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
await input_task
if ui_task:
await ui_task
if input_task:
input_task.cancel()
class FactorioJSONtoTextParser(JSONtoTextParser):

View File

@@ -13,35 +13,8 @@ os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
import asyncio
from CommonClient import server_loop, logger
from FactorioClient import FactorioContext, factorio_server_watcher
async def main():
ctx = FactorioContext(None, None, True)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
ui_app = FactorioManager(ctx)
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
await ctx.exit_event.wait() # wait for signal to exit application
ui_app.stop()
ctx.server_address = None
ctx.snes_reconnect_address = None
# allow tasks to quit
await ui_task
await factorio_server_task
await ctx.server_task
if ctx.server is not None and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task is not None:
await ctx.server_task
while ctx.input_requests > 0: # clear queue for shutdown
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
from CommonClient import logger
from FactorioClient import main
from kivy.app import App
@@ -59,7 +32,7 @@ class FactorioManager(App):
super(FactorioManager, self).__init__()
self.ctx = ctx
self.commandprocessor = ctx.command_processor(ctx)
self.icon = "data/icon.png"
self.icon = r"data/icon.png"
def build(self):
self.grid = GridLayout()
@@ -70,7 +43,7 @@ class FactorioManager(App):
pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge File Log"),
("FactorioWatcher", "Bridge Data Log"),
]
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
for logger_name, display_name in pairs:
@@ -163,5 +136,6 @@ Builder.load_string('''
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
ui_app = FactorioManager
loop.run_until_complete(main(ui_app))
loop.close()

View File

@@ -68,7 +68,6 @@ class Context(CommonContext):
self.snes_reconnect_address = None
self.snes_recv_queue = asyncio.Queue()
self.snes_request_lock = asyncio.Lock()
self.is_sd2snes = False
self.snes_write_buffer = []
self.awaiting_rom = False
@@ -408,26 +407,30 @@ class SNESState(enum.IntEnum):
SNES_ATTACHED = 3
def launch_qusb2snes(ctx: Context):
qusb2snes_path = Utils.get_options()["lttp_options"]["qusb2snes"]
def launch_sni(ctx: Context):
sni_path = Utils.get_options()["lttp_options"]["sni"]
if not os.path.isfile(qusb2snes_path):
qusb2snes_path = Utils.local_path(qusb2snes_path)
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
if os.path.isdir(sni_path):
for file in os.listdir(sni_path):
if file.startswith("sni.") and not file.endswith(".proto"):
sni_path = os.path.join(sni_path, file)
if os.path.isfile(qusb2snes_path):
logger.info(f"Attempting to start {qusb2snes_path}")
if os.path.isfile(sni_path):
logger.info(f"Attempting to start {sni_path}")
import subprocess
subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path))
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
logger.info(
f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, "
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
f"please start it yourself if it is not running")
async def _snes_connect(ctx: Context, address: str):
address = f"ws://{address}" if "://" not in address else address
logger.info("Connecting to QUsb2snes at %s ..." % address)
logger.info("Connecting to SNI at %s ..." % address)
seen_problems = set()
succesful = False
while not succesful:
@@ -439,11 +442,11 @@ async def _snes_connect(ctx: Context, address: str):
# only tell the user about new problems, otherwise silently lay in wait for a working connection
if problem not in seen_problems:
seen_problems.add(problem)
logger.error(f"Error connecting to QUsb2snes ({problem})")
logger.error(f"Error connecting to SNI ({problem})")
if len(seen_problems) == 1:
# this is the first problem. Let's try launching QUsb2snes if it isn't already running
launch_qusb2snes(ctx)
# this is the first problem. Let's try launching SNI if it isn't already running
launch_sni(ctx)
await asyncio.sleep(1)
else:
@@ -462,7 +465,7 @@ async def get_snes_devices(ctx: Context):
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
if not devices:
logger.info('No SNES device found. Ensure QUsb2Snes is running and connect it to the multibridge.')
logger.info('No SNES device found. Please connect a SNES device to SNI.')
while not devices:
await asyncio.sleep(1)
await socket.send(dumps(DeviceList_Request))
@@ -510,17 +513,6 @@ async def snes_connect(ctx: Context, address):
await ctx.snes_socket.send(dumps(Attach_Request))
ctx.snes_state = SNESState.SNES_ATTACHED
ctx.snes_attached_device = (devices.index(device), device)
if 'sd2snes' in device.lower() or 'COM' in device:
logger.info("SD2SNES/FXPAK Detected")
ctx.is_sd2snes = True
await ctx.snes_socket.send(dumps({"Opcode": "Info", "Space": "SNES"}))
reply = loads(await ctx.snes_socket.recv())
if reply and 'Results' in reply:
logger.info(reply['Results'])
else:
ctx.is_sd2snes = False
ctx.snes_reconnect_address = address
recv_task = asyncio.create_task(snes_recv_loop(ctx))
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
@@ -614,8 +606,7 @@ async def snes_read(ctx: Context, address, size):
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
if len(data):
logger.error(str(data))
logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
'Try un-selecting and re-selecting the SNES Device.')
logger.warning('Communication Failure with SNI')
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
return None
@@ -634,45 +625,16 @@ async def snes_write(ctx: Context, write_list):
return False
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
if ctx.is_sd2snes:
cmd = b'\x00\xE2\x20\x48\xEB\x48'
try:
for address, data in write_list:
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
logger.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
return False
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
cmd += b'\xA9' # LDA
cmd += bytes([byte])
cmd += b'\x8F' # STA.l
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
PutAddress_Request['Space'] = 'CMD'
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd) - 1)[2:], "2C00", "1"]
try:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(cmd)
await ctx.snes_socket.send(data)
else:
logger.warning(f"Could not send data to SNES: {cmd}")
except websockets.ConnectionClosed:
return False
else:
PutAddress_Request['Space'] = 'SNES'
try:
# will pack those requests as soon as qusb2snes actually supports that for real
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
else:
logger.warning(f"Could not send data to SNES: {data}")
except websockets.ConnectionClosed:
return False
logger.warning(f"Could not send data to SNES: {data}")
except websockets.ConnectionClosed:
return False
return True
finally:
@@ -852,10 +814,11 @@ async def game_watcher(ctx: Context):
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received)))
recv_index += 1
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
@@ -886,7 +849,7 @@ async def main():
parser = argparse.ArgumentParser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
@@ -909,14 +872,12 @@ async def main():
logging.exception(e)
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
port = None
ctx = Context(args.snes, args.connect, args.password, args.founditems)
input_task = asyncio.create_task(console_loop(ctx), name="Input")
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
asyncio.create_task(snes_connect(ctx, ctx.snes_address))
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
await ctx.exit_event.wait()

36
Main.py
View File

@@ -21,10 +21,6 @@ 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
from worlds.hk import create_regions as hk_create_regions
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, lookup_any_item_name_to_id, AutoWorld
import Patch
@@ -196,14 +192,8 @@ def main(args, seed=None):
world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals']
for player in world.hk_player_ids:
hk_create_regions(world, player)
AutoWorld.call_all(world, "create_regions")
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',
@@ -263,14 +253,8 @@ def main(args, seed=None):
for player in world.alttp_player_ids:
set_rules(world, player)
for player in world.hk_player_ids:
gen_hollow(world, player)
AutoWorld.call_all(world, "generate_basic")
for player in world.minecraft_player_ids:
gen_minecraft(world, player)
logger.info("Running Item Plando")
for item in world.itempool:
@@ -497,10 +481,7 @@ def main(args, seed=None):
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {}
for slot in world.player_ids:
if world.game[slot] == "Factorio":
client_versions[slot] = (0, 1, 2)
else:
client_versions[slot] = (0, 0, 3)
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}
@@ -519,14 +500,10 @@ def main(args, seed=None):
if player not in world.alttp_player_ids:
connect_names[name] = (i, player)
if world.hk_player_ids:
import Options
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)
slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot)
for slot in world.minecraft_player_ids:
slot_data[slot] = fill_minecraft_slot_data(world, slot)
slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot)
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
@@ -578,8 +555,6 @@ 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)
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
@@ -697,8 +672,9 @@ def create_playthrough(world):
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
world.spoiler.paths = dict()
for player in range(1, world.players + 1):
world.spoiler.paths = {}
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
for player in topology_worlds:
world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})

View File

@@ -6,6 +6,7 @@ import concurrent.futures
import argparse
import logging
import random
from shutil import which
def feedback(text: str):
@@ -76,8 +77,10 @@ if __name__ == "__main__":
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
elif os.path.exists("ArchipelagoMystery"):
basemysterycommand = "./ArchipelagoMystery" # compiled linux
elif which('py'):
basemysterycommand = f"py -{py_version} Mystery.py" # source windows
else:
basemysterycommand = f"py -{py_version} Mystery.py" # source
basemysterycommand = f"python3 Mystery.py" # source others
weights_file_path = os.path.join(player_files_path, weights_file_path)
if os.path.exists(weights_file_path):
@@ -210,8 +213,10 @@ if __name__ == "__main__":
baseservercommand = ["ArchipelagoServer.exe"] # compiled windows
elif os.path.exists("ArchipelagoServer"):
baseservercommand = ["./ArchipelagoServer"] # compiled linux
elif which('py'):
baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source windows
else:
baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source
baseservercommand = ["python3", "MultiServer.py"] # source others
# don't have a mac to test that. If you try to run compiled on mac, good luck.
subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)])
except:

View File

@@ -244,7 +244,15 @@ class Context(Node):
import atexit
atexit.register(self._save, True) # make sure we save on exit too
def recheck_hints(self):
for team, slot in self.hints:
self.hints[team, slot] = {
hint.re_check(self, team) for hint in
self.hints[team, slot]
}
def get_save(self) -> dict:
self.recheck_hints()
d = {
"connect_names": self.connect_names,
"received_items": self.received_items,
@@ -561,7 +569,8 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
NetUtils.add_json_text(parts, ")")
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
"receiving": receiving_player, "sending": net_item.player}
"receiving": receiving_player,
"item": net_item}
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str] = all_console_names) -> typing.Tuple[
@@ -1026,7 +1035,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
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}")
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
else:
ctx.client_ids[client.team, client.slot] = args["uuid"]

View File

@@ -9,6 +9,7 @@ from collections import Counter
import string
import ModuleUpdate
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoItem, PlandoConnection
ModuleUpdate.update()
@@ -546,49 +547,38 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.startinventory = startitems
ret.start_hints = set(game_weights.get('start_hints', []))
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_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, game_weights, True)))
elif ret.game == "Factorio":
for option_name, option in Options.factorio_options.items():
if ret.game in AutoWorldRegister.world_types:
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
if option_name in game_weights:
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
setattr(ret, option_name, option.from_any(game_weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
try:
if issubclass(option, Options.OptionDict):
setattr(ret, option_name, option.from_any(game_weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
except Exception as e:
raise Exception(f"Error generating option {option_name} in {ret.game}")
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 game_weights:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
else:
setattr(ret, option_name, option(option.default))
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
))
if ret.game == "Minecraft":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
else:
raise Exception(f"Unsupported game {ret.game}")
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
for option_name, option in Options.alttp_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))
glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
@@ -639,7 +629,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = Options.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
@@ -647,7 +637,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = Options.TriforcePieces.from_any(
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':

View File

@@ -307,7 +307,9 @@ class Hint(typing.NamedTuple):
else:
add_json_text(parts, ".")
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
"item": NetworkItem(self.item, self.location, self.finding_player)}
@property
def local(self):

View File

@@ -21,38 +21,6 @@ class AssembleOptions(type):
name.startswith("alias_")})
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
class AssembleCategoryPath(type):
def __new__(mcs, name, bases, attrs):
path = []
for base in bases:
if hasattr(base, "segment"):
path += base.segment
path += attrs["segment"]
attrs["path"] = path
return super(AssembleCategoryPath, mcs).__new__(mcs, name, bases, attrs)
class RootCategory(metaclass=AssembleCategoryPath):
segment = []
class LttPCategory(RootCategory):
segment = ["A Link to the Past"]
class LttPRomCategory(LttPCategory):
segment = ["rom"]
class FactorioCategory(RootCategory):
segment = ["Factorio"]
class MinecraftCategory(RootCategory):
segment = ["Minecraft"]
class Option(metaclass=AssembleOptions):
value: int
name_lookup: typing.Dict[int, str]
@@ -175,6 +143,9 @@ class Range(Option, int):
return cls(data)
return cls.from_text(str(data))
def get_option_name(self):
return str(self.value)
def __str__(self):
return str(self.value)
@@ -213,243 +184,18 @@ class OptionDict(Option):
return str(self.value)
class Logic(Choice):
option_no_glitches = 0
option_minor_glitches = 1
option_overworld_glitches = 2
option_hybrid_major_glitches = 3
option_no_logic = 4
alias_owg = 2
alias_hmg = 3
class Objective(Choice):
option_crystals = 0
# option_pendants = 1
option_triforce_pieces = 2
option_pedestal = 3
option_bingo = 4
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
class Goal(Choice):
option_kill_ganon = 0
option_kill_ganon_and_gt_agahnim = 1
option_hand_in = 2
class Accessibility(Choice):
option_locations = 0
option_items = 1
option_beatable = 2
class Crystals(Range):
range_start = 0
range_end = 7
class CrystalsTower(Crystals):
default = 7
class CrystalsGanon(Crystals):
default = 7
class TriforcePieces(Range):
default = 30
range_start = 1
range_end = 90
class ShopItemSlots(Range):
range_start = 0
range_end = 30
class WorldState(Choice):
option_standard = 1
option_open = 0
option_inverted = 2
class Bosses(Choice):
option_vanilla = 0
option_simple = 1
option_full = 2
option_chaos = 3
option_singularity = 4
class Enemies(Choice):
option_vanilla = 0
option_shuffled = 1
option_chaos = 2
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"shop_item_slots": ShopItemSlots,
}
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {
"RandomizeDreamers": DefaultOnToggle,
"RandomizeSkills": DefaultOnToggle,
"RandomizeCharms": DefaultOnToggle,
"RandomizeKeys": DefaultOnToggle,
"RandomizeGeoChests": Toggle,
"RandomizeMaskShards": DefaultOnToggle,
"RandomizeVesselFragments": DefaultOnToggle,
"RandomizeCharmNotches": Toggle,
"RandomizePaleOre": DefaultOnToggle,
"RandomizeRancidEggs": Toggle,
"RandomizeRelics": DefaultOnToggle,
"RandomizeMaps": Toggle,
"RandomizeStags": Toggle,
"RandomizeGrubs": Toggle,
"RandomizeWhisperingRoots": Toggle,
"RandomizeRocks": Toggle,
"RandomizeSoulTotems": Toggle,
"RandomizePalaceTotems": Toggle,
"RandomizeLoreTablets": Toggle,
"RandomizeLifebloodCocoons": Toggle,
"RandomizeFlames": Toggle
}
hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
"MILDSKIPS": Toggle,
"SPICYSKIPS": Toggle,
"FIREBALLSKIPS": Toggle,
"ACIDSKIPS": Toggle,
"SPIKETUNNELS": Toggle,
"DARKROOMS": Toggle,
"CURSED": Toggle,
"SHADESKIPS": Toggle,
}
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} - \
{"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense
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_large_diamonds = 3
option_small_pyramids = 4
option_medium_pyramids = 5
option_large_pyramids = 6
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_funnels = 4
alias_pyramid = 6
alias_funnel = 9
default = 0
class TechTreeInformation(Choice):
option_none = 0
option_advancement = 1
option_full = 2
default = 2
class RecipeTime(Choice):
option_vanilla = 0
option_fast = 1
option_normal = 2
option_slow = 4
option_chaos = 5
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,
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"recipe_time": RecipeTime,
"imported_blueprints": DefaultOnToggle,
}
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
}
option_sets = (
minecraft_options,
factorio_options,
alttp_options,
hollow_knight_options
)
if __name__ == "__main__":
from worlds.alttp.Options import Logic
import argparse
mapshuffle = Toggle
compassshuffle = Toggle

164
Utils.py
View File

@@ -12,7 +12,8 @@ class Version(typing.NamedTuple):
minor: int
build: int
__version__ = "0.1.3"
__version__ = "0.1.4"
version_tuple = tuplize_version(__version__)
import builtins
@@ -22,6 +23,7 @@ import sys
import pickle
import functools
import io
import collections
from yaml import load, dump, safe_load
@@ -52,7 +54,6 @@ def snes_to_pc(value):
def parse_player_names(names, players, teams):
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
if len(names) != len(set(names)):
import collections
name_counter = collections.Counter(names)
raise ValueError(f"Duplicate Player names is not supported, "
f'found multiple "{name_counter.most_common(1)[0][0]}".')
@@ -68,6 +69,21 @@ def parse_player_names(names, players, teams):
return ret
def cache_argsless(function):
if function.__code__.co_argcount:
raise Exception("Can only cache 0 argument functions with this cache.")
result = sentinel = object()
def _wrap():
nonlocal result
if result is sentinel:
result = function()
return result
return _wrap
def is_bundled() -> bool:
return getattr(sys, 'frozen', False)
@@ -118,20 +134,10 @@ def open_file(filename):
subprocess.call([open_command, filename])
def close_console():
if sys.platform == 'win32':
# windows
import ctypes.wintypes
try:
ctypes.windll.kernel32.FreeConsole()
except Exception:
pass
parse_yaml = safe_load
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
@cache_argsless
def get_public_ipv4() -> str:
import socket
import urllib.request
@@ -147,7 +153,7 @@ def get_public_ipv4() -> str:
pass # we could be offline, in a local game, so no point in erroring out
return ip
@cache_argsless
def get_public_ipv6() -> str:
import socket
import urllib.request
@@ -160,70 +166,68 @@ def get_public_ipv6() -> str:
pass # we could be offline, in a local game, or ipv6 may not be available
return ip
@cache_argsless
def get_default_options() -> dict:
if not hasattr(get_default_options, "options"):
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
"rom_start": True,
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"sni": "SNI",
"rom_start": True,
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"multi_mystery_options": {
"teams": 1,
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"pre_roll": False,
"create_spoiler": 1,
"zip_roms": 0,
"zip_diffs": 2,
"zip_apmcs": 1,
"zip_spoiler": 0,
"zip_multidata": 1,
"zip_format": 1,
"glitch_triforce_room": 1,
"race": 0,
"cpu_threads": 0,
"max_attempts": 0,
"take_first_working": False,
"keep_all_seeds": False,
"log_output_path": "Output Logs",
"log_level": None,
"plando_options": "bosses",
}
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"multi_mystery_options": {
"teams": 1,
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"pre_roll": False,
"create_spoiler": 1,
"zip_roms": 0,
"zip_diffs": 2,
"zip_apmcs": 1,
"zip_spoiler": 0,
"zip_multidata": 1,
"zip_format": 1,
"glitch_triforce_room": 1,
"race": 0,
"cpu_threads": 0,
"max_attempts": 0,
"take_first_working": False,
"keep_all_seeds": False,
"log_output_path": "Output Logs",
"log_level": None,
"plando_options": "bosses",
}
}
get_default_options.options = options
return get_default_options.options
return options
blacklisted_options = {"multi_mystery_options.cpu_threads",
@@ -253,7 +257,7 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
dest[key] = update_options(value, dest[key], filename, new_keys)
return dest
@cache_argsless
def get_options() -> dict:
if not hasattr(get_options, "options"):
locations = ("options.yaml", "host.yaml",
@@ -367,7 +371,7 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
return romfile, adjusted
return romfile, False
@cache_argsless
def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None)
if uuid:
@@ -404,4 +408,10 @@ class RestrictedUnpickler(pickle.Unpickler):
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
return RestrictedUnpickler(io.BytesIO(s)).load()
class KeyedDefaultDict(collections.defaultdict):
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value

View File

@@ -6,7 +6,6 @@ import socket
import jinja2.exceptions
from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask import Blueprint
from flask_caching import Cache
from flask_compress import Compress

View File

@@ -7,6 +7,7 @@ import concurrent.futures
import sys
import typing
import time
import os
from pony.orm import db_session, select, commit
@@ -15,10 +16,13 @@ from Utils import restricted_loads
class CommonLocker():
"""Uses a file lock to signal that something is already running"""
def __init__(self, lockname: str):
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
os.makedirs(self.lock_folder, exist_ok=True)
self.lockname = lockname
self.lockfile = f"./{self.lockname}.lck"
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
class AlreadyRunningException(Exception):
@@ -26,9 +30,6 @@ class AlreadyRunningException(Exception):
if sys.platform == 'win32':
import os
class Locker(CommonLocker):
def __enter__(self):
try:

View File

@@ -104,6 +104,7 @@ class WebHostContext(Context):
def get_random_port():
return random.randint(49152, 65535)
def run_server_process(room_id, ponyconfig: dict):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
@@ -144,7 +145,9 @@ def run_server_process(room_id, ponyconfig: dict):
await ctx.shutdown_task
logging.info("Shutting down")
asyncio.run(main())
from .autolauncher import Locker
with Locker(room_id):
asyncio.run(main())
from WebHostLib import LOGS_FOLDER

View File

@@ -2,5 +2,5 @@ flask>=2.0.1
pony>=0.7.14
waitress>=2.0.0
flask-caching>=1.10.1
Flask-Compress>=1.9.0
Flask-Compress>=1.10.1
Flask-Limiter>=1.4

View File

@@ -17,6 +17,47 @@ window.addEventListener('load', () => {
paging: false,
info: false,
dom: "t",
columnDefs: [
{
targets: 'hours',
render: function (data, type, row) {
if (type === "sort" || type === 'type') {
if (data === "None")
return -1;
return parseInt(data);
}
if (data === "None")
return data;
let hours = Math.floor(data / 3600);
let minutes = Math.floor((data - (hours * 3600)) / 60);
if (minutes < 10) {minutes = "0"+minutes;}
return hours+':'+minutes;
}
},
{
targets: 'number',
render: function (data, type, row) {
if (type === "sort" || type === 'type') {
return parseFloat(data);
}
return data;
}
},
{
targets: 'fraction',
render: function (data, type, row) {
let splitted = data.split("/", 1);
let current = splitted[0]
if (type === "sort" || type === 'type') {
return parseInt(current);
}
return data;
}
},
],
// DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from
// the tbody and render two separate tables.

View File

@@ -33,19 +33,17 @@ use-system-read-write-data-directories=false
## Joining a MultiWorld Game
1. Make a fresh world. Using any Factorio, create a savegame with options of your choice and save that world as Archipelago.
1. Install the generated Factorio AP Mod (would be in <Factorio Directory>/Mods after step 2 of Setup)
2. Take that savegame and put it into your Archipelago folder
3. Install the generated Factorio AP Mod
4. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
2. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
* It should say it loaded the Archipelago mod and found a bridge file. If not, the most likely case is that the mod is not correctly installed or activated.
* It should start up, create a world and become ready for Factorio connections.
5. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
3. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
* / commands are run on your local client, ! commands are requests for the AP server
* Players should be able to connect to your Factorio Server and begin playing.
4. You can join yourself by connecting to address `localhost`, other people will need to connect to your IP
and you may need to port forward for the Factorio Server for those connections.

View File

@@ -102,7 +102,6 @@ html{
width: 260px;
height: calc(130px - 35px);
padding-top: 35px;
cursor: default;
}
#landing-clouds{

View File

@@ -24,7 +24,7 @@
height: 100%;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%);
filter: grayscale(100%) contrast(75%) brightness(75%);
}
#inventory-table img.acquired{

View File

@@ -16,7 +16,7 @@
<a href="/games" id="mid-button">start<br />playing</a>
<a id="far-left-button"></a>
<a href="/tutorial" id="mid-left-button">setup guide</a>
<a id="far-right-button"></a>
<a href="/uploads" id="far-right-button">Host Game</a>
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
</div>
<div id="landing-clouds">

View File

@@ -98,20 +98,20 @@
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
{%- endif -%}
{%- endfor -%}
<th rowspan="2" class="center-column">Last<br>Activity</th>
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
</tr>
<tr>
{% for area in ordered_areas %}
<th class="center-column lower-row">
<th class="center-column lower-row fraction">
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
</th>
{% if area in key_locations %}
<th class="center-column lower-row">
<th class="center-column lower-row number">
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
</th>
{% endif %}
{% if area in big_key_locations %}
<th class="center-column lower-row">
<th class="center-column lower-row number">
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
</th>
{%- endif -%}
@@ -141,7 +141,7 @@
{%- endif -%}
{%- endfor -%}
{%- if activity_timers[(team, player)] -%}
<td class="center-column">{{ activity_timers[(team, player)] | render_timedelta }}</td>
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
{%- else -%}
<td class="center-column">None</td>
{%- endif -%}

View File

@@ -20,4 +20,16 @@ function get_any_stack_size(name)
end
-- failsafe
return 1
end
-- from https://stackoverflow.com/a/40180465
-- split("a,b,c", ",") => {"a", "b", "c"}
function split(s, sep)
local fields = {}
sep = sep or " "
local pattern = string.format("([^%s]+)", sep)
string.gsub(s, pattern, function(c) fields[#fields + 1] = c end)
return fields
end

View File

@@ -6,6 +6,7 @@ require "util"
FREE_SAMPLES = {{ free_samples }}
SLOT_NAME = "{{ slot_name }}"
SEED_NAME = "{{ seed_name }}"
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
{% if not imported_blueprints -%}
function set_permissions()
@@ -140,69 +141,49 @@ script.on_init(function()
end
end)
-- for testing
-- script.on_event(defines.events.on_tick, function(event)
-- if event.tick%3600 == 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
if technology.researched and string.find(technology.name, "ap%-") == 1 then
dumpInfo(technology.force) --is sendable
end
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)
else
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
if FREE_SAMPLE_BLACKLIST[name] ~= 1 then
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
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,
["seed_name"] = SEED_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)
log("Archipelago Bridge File written for game tick ".. game.tick .. ".")
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
-- game.print("Sent progress to Archipelago.")
log("Archipelago Bridge Data available for game tick ".. game.tick .. ".") -- notifies client
end
function chain_lookup(table, ...)
for _, k in ipairs{...} do
table = table[k]
@@ -213,33 +194,76 @@ function chain_lookup(table, ...)
return table
end
-- add / commands
commands.add_command("ap-sync", "Run manual Research Sync with Archipelago.", function(call)
-- add / commands
commands.add_command("ap-sync", "Used by the Archipelago client to get progress information", function(call)
local force
if call.player_index == nil then
dumpInfo(game.forces.player)
force = game.forces.player
else
dumpInfo(game.players[call.player_index].force)
force = game.players[call.player_index].force
end
game.print("Wrote bridge file.")
local research_done = {}
local data_collection = {
["research_done"] = research_done,
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
}
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
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection}))
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)
if global.index_sync == nil then
global.index_sync = {}
end
local tech
local force = game.forces["player"]
chunks = split(call.parameter, "\t")
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
local index = chunks[2]
local source = chunks[3] or "Archipelago"
if progressive_technologies[tech_name] ~= nil then
if global.index_sync[index] == nil then -- not yet received prog item
global.index_sync[index] = tech_name
local tech_stack = progressive_technologies[tech_name]
for _, tech_name in ipairs(tech_stack) do
tech = force.technologies[tech_name]
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
return
end
end
end
elseif force.technologies[tech_name] ~= nil then
tech = force.technologies[tech_name]
if tech ~= nil then
if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then
game.print("Warning: Desync Detected. Duplicate/Missing items may occur.")
end
global.index_sync[index] = tech
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
end
else
game.print("Unknown Technology " .. tech_name)
end
end)
end)
commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call)
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME}))
end)
-- data
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}

View File

@@ -2,7 +2,9 @@
-- 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 = {{ dict_to_recipe(rocket_recipe) }}
{%- for recipe_name, recipe in custom_recipes.items() %}
data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ingredients) }}
{%- endfor %}
local technologies = data.raw["technology"]
local original_tech
@@ -50,14 +52,25 @@ function copy_factorio_icon(tech, tech_source)
end
function adjust_energy(recipe_name, factor)
local energy = data.raw.recipe[recipe_name].energy_required
if (energy == nil) then
energy = 1
local recipe = data.raw.recipe[recipe_name]
local energy = recipe.energy_required
if (energy ~= nil) then
data.raw.recipe[recipe_name].energy_required = energy * factor
end
if (recipe.normal ~= nil and recipe.normal.energy_required ~= nil) then
energy = recipe.normal.energy_required
recipe.normal.energy_required = energy * factor
end
if (recipe.expensive ~= nil and recipe.expensive.energy_required ~= nil) then
energy = recipe.expensive.energy_required
recipe.expensive.energy_required = energy * factor
end
data.raw.recipe[recipe_name].energy_required = energy * factor
end
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
@@ -69,9 +82,11 @@ prep_copy(new_tree_copy, original_tech)
{% if tech_cost_scale != 1 %}
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
{% endif %}
{%- if item_name in tech_table and tech_tree_information == 2 -%}
{%- if (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in base_tech_table -%}
{#- copy Factorio Technology Icon -#}
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%}
copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}")
{%- else -%}
{#- use default AP icon if no Factorio graphics exist -#}
{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
@@ -86,7 +101,9 @@ table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
data:extend{new_tree_copy}
{% endfor %}
{% if recipe_time_scale %}
{%- for recipe in recipes %}
adjust_energy("{{ recipe }}", {{ random.triangular(*recipe_time_scale) }})
{%- for recipe_name, recipe in recipes.items() %}
{%- if recipe.category != "mining" %}
adjust_energy("{{ recipe_name }}", {{ random.triangular(*recipe_time_scale) }})
{%- endif %}
{%- endfor -%}
{% endif %}

View File

@@ -0,0 +1,2 @@
{% from "macros.lua" import dict_to_lua %}
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen}) }}

View File

@@ -1,20 +1,25 @@
[map-gen-preset-name]
archipelago=Archipelago
[map-gen-preset-description]
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
[technology-name]
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if tech_tree_information == 2 -%}
{%- if tech_tree_information == 2 or original_tech_name in static_nodes %}
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
{% else %}
{%- else %}
ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable
{%- endif -%}
{% endfor %}
[technology-description]
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if tech_tree_information == 2 %}
{%- if tech_tree_information == 2 or original_tech_name in static_nodes %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}.
{%- elif tech_tree_information == 1 and advancement %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement.
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}".
{%- else %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}".
{%- endif -%}
{% endfor %}

View File

@@ -1,10 +1,25 @@
{% macro dict_to_lua(dict) -%}
{
{%- for key, value in dict.items() -%}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %}
{% endfor -%}
}
{%- endmacro %}
{% macro list_to_lua(list) -%}
{
{%- for key in list -%}
{{ variable_to_lua(key) }}{% if not loop.last %},{% endif %}
{% endfor -%}
}
{%- endmacro %}
{%- macro variable_to_lua(value) %}
{%- if value is mapping -%}{{ dict_to_lua(value) }}
{%- elif value is boolean -%}{{ value | string | lower }}
{%- elif value is string -%}"{{ value | safe }}"
{%- elif value is iterable -%}{{ list_to_lua(value) }}
{%- else -%} {{ value | safe }}
{%- endif -%}
{%- endmacro -%}
{% macro dict_to_recipe(dict) -%}
{
{%- for key, value in dict.items() -%}

File diff suppressed because one or more lines are too long

View File

@@ -109,8 +109,8 @@ multi_mystery_options:
lttp_options:
# File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
# Set this to your (Q)Usb2Snes location if you want the MultiClient to attempt an auto start, does nothing if not found
qusb2snes: "QUsb2Snes\\QUsb2Snes.exe"
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with

View File

@@ -70,6 +70,9 @@ Factorio:
normal: 0 # 50 % to 200% of original time
slow: 0 # 100% to 400% of original time
chaos: 0 # 25% to 400% of original time
recipe_ingredients:
rocket: 1 # only randomize rocket part recipe
science_pack: 1 # also randomize science pack ingredients
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0
@@ -91,6 +94,9 @@ Factorio:
single_craft: 0
half_stack: 0
stack: 0
progressive:
on: 1
off: 0
tech_tree_information:
none: 0
advancement: 0 # show which items are a logical advancement
@@ -102,10 +108,7 @@ Factorio:
burner-mining-drill: 19
stone-furnace: 19
Minecraft:
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
advancement_goal: 50 # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
easy: 0
normal: 1
@@ -119,7 +122,7 @@ Minecraft:
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.
shuffle_structures: # Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
on: 0
off: 1
A Link to the Past:

View File

@@ -2,6 +2,6 @@ colorama>=0.4.4
websockets>=9.1
PyYAML>=5.4.1
fuzzywuzzy>=0.18.0
prompt_toolkit>=3.0.18
prompt_toolkit>=3.0.19
appdirs>=1.4.4
jinja2>=3.0.1

View File

@@ -88,7 +88,7 @@ cx_Freeze.setup(
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": [],
"zip_exclude_packages": ["worlds"],
"include_files": [],
"include_msvcr": True,
"replace_paths": [("*", "")],
@@ -113,7 +113,7 @@ def installfile(path, keep_content=False):
print('Warning,', path, 'not found')
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "QUsb2Snes", "meta.yaml"]
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
for data in extra_data:
installfile(Path(data))
@@ -131,30 +131,12 @@ else:
file = z3pr.__file__
installfile(Path(os.path.dirname(file)) / "data", keep_content=True)
qusb2sneslog = buildfolder / "QUsb2Snes" / "log.txt"
if os.path.exists(qusb2sneslog):
os.remove(qusb2sneslog)
qusb2snesconfig = buildfolder / "QUsb2Snes" / "config.ini"
# turns on all bridges, disables auto update
with open(qusb2snesconfig, "w") as f:
f.write("""[General]
SendToSet=true
checkUpdateCounter=20
luabridge=true
LuaBridgeRNGSeed=79120361805329566567327599
FirstTime=true
sd2snessupport=true
retroarchdevice=true
snesclassic=true""")
if signtool:
for exe in exes:
print(f"Signing {exe.target_name}")
os.system(signtool + os.path.join(buildfolder, exe.target_name))
print(f"Signing QUsb2Snes")
os.system(signtool + os.path.join(buildfolder, "Qusb2Snes", "QUsb2Snes.exe"))
print(f"Signing SNI")
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
for file in os.listdir(alttpr_sprites_folder):
@@ -204,7 +186,7 @@ cx_Freeze.setup(
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["kivy"],
"zip_exclude_packages": ["kivy", "worlds"],
"include_files": [],
"include_msvcr": True,
"replace_paths": [("*", "")],

View File

@@ -1,4 +1,5 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
@@ -8,14 +9,16 @@ from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from Options import alttp_options
from worlds import AutoWorld
class TestDungeon(unittest.TestCase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits
self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -1,6 +1,7 @@
from worlds.hk import HKWorld
from BaseClasses import MultiWorld
from worlds.hk.Regions import create_regions
from worlds.hk import gen_hollow
from worlds import AutoWorld
from worlds.hk.Options import hollow_knight_randomize_options, hollow_knight_skip_options
from test.TestBase import TestBase
@@ -9,10 +10,11 @@ class TestVanilla(TestBase):
def setUp(self):
self.world = MultiWorld(1)
self.world.game[1] = "Hollow Knight"
import Options
for hk_option in Options.hollow_knight_randomize_options:
self.world.worlds[1] = HKWorld(self.world, 1)
for hk_option in hollow_knight_randomize_options:
setattr(self.world, hk_option, {1: True})
for hk_option, option in Options.hollow_knight_skip_options.items():
for hk_option, option in hollow_knight_skip_options.items():
setattr(self.world, hk_option, {1: option.default})
create_regions(self.world, 1)
gen_hollow(self.world, 1)
AutoWorld.call_single(self.world, "create_regions", 1)
AutoWorld.call_single(self.world, "generate_basic", 1)
AutoWorld.call_single(self.world, "set_rules", 1)

View File

@@ -1,3 +1,5 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
@@ -8,13 +10,16 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from Options import alttp_options
from worlds import AutoWorld
class TestInverted(TestBase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.difficulty_requirements[1] = difficulties['normal']
self.world.mode[1] = "inverted"
create_inverted_regions(self.world, 1)

View File

@@ -1,3 +1,5 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
@@ -8,13 +10,16 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from Options import alttp_options
from worlds import AutoWorld
class TestInvertedMinor(TestBase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.mode[1] = "inverted"
self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -1,3 +1,5 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
@@ -8,14 +10,17 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from Options import alttp_options
from worlds import AutoWorld
class TestInvertedOWG(TestBase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.logic[1] = "owglitches"
self.world.mode[1] = "inverted"
self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -1,10 +1,11 @@
import worlds.minecraft.Options
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 import AutoWorld
from worlds.minecraft import MinecraftWorld
from worlds.minecraft.Items import MinecraftItem, item_table
import Options
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty
from Options import Toggle
# Converts the name of an item into an item object
def MCItemFactory(items, player: int):
@@ -28,16 +29,17 @@ class TestMinecraft(TestBase):
def setUp(self):
self.world = MultiWorld(1)
self.world.game[1] = "Minecraft"
self.world.worlds[1] = MinecraftWorld(self.world, 1)
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)
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
setattr(self.world, "shuffle_structures", {1: Toggle(False)})
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
setattr(self.world, "bee_traps", {1: Toggle(False)})
AutoWorld.call_single(self.world, "create_regions", 1)
AutoWorld.call_single(self.world, "generate_basic", 1)
AutoWorld.call_single(self.world, "set_rules", 1)
def _get_items(self, item_pool, all_except):
if all_except and len(all_except) > 0:

View File

@@ -1,3 +1,5 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances
@@ -8,13 +10,16 @@ from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from Options import alttp_options
from worlds import AutoWorld
class TestMinor(TestBase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1)

View File

@@ -1,3 +1,5 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances
@@ -8,14 +10,17 @@ from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from Options import alttp_options
from worlds import AutoWorld
class TestVanillaOWG(TestBase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.difficulty_requirements[1] = difficulties['normal']
self.world.logic[1] = "owglitches"
create_regions(self.world, 1)

View File

@@ -1,3 +1,5 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances
@@ -8,13 +10,15 @@ from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from Options import alttp_options
from worlds import AutoWorld
class TestVanilla(TestBase):
def setUp(self):
self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.logic[1] = "noglitches"
self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1)

View File

@@ -27,20 +27,37 @@ class World(metaclass=AutoWorldRegister):
world: MultiWorld
player: int
options: dict = {}
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
def __init__(self, world: MultiWorld, player: int):
self.world = world
self.player = player
# overwritable methods that get called by Main.py
def generate_basic(self):
# overwritable methods that get called by Main.py, sorted by execution order
def create_regions(self):
pass
def set_rules(self):
pass
def create_regions(self):
def generate_basic(self):
pass
def generate_output(self):
"""This method gets called from a threadpool, do not use world.random here.
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
pass
def get_required_client_version(self) -> tuple:
return 0, 0, 3
# end of Main.py calls
def collect(self, state, item) -> bool:
"""Collect an item into state. For speed reasons items that aren't logically useful get skipped."""
if item.advancement:
state.prog_items[item.name, item.player] += 1
return True # indicate that a logical state change has occured
return False

View File

@@ -1,10 +1,13 @@
import enum
import importlib
import os
__all__ = {"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package",
"Games"}
# all of the below should be moved to AutoWorld functionality
from .alttp.Items import lookup_id_to_name as alttp
from .hk.Items import lookup_id_to_name as hk
from .factorio import Technologies
@@ -29,7 +32,7 @@ assert len(lookup_any_location_name_to_id) == len(lookup_any_location_id_to_name
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": 6}
"version": 9}
@enum.unique
@@ -38,3 +41,11 @@ class Games(str, enum.Enum):
LTTP = "A Link to the Past"
Factorio = "Factorio"
Minecraft = "Minecraft"
# end of TODO block
# import all submodules to trigger AutoWorldRegister
for file in os.scandir(os.path.dirname(__file__)):
if file.is_dir():
importlib.import_module(f".{file.name}", "worlds")

View File

@@ -9,7 +9,6 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
from worlds.generic.Rules import forbid_items_for_player
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -349,9 +348,7 @@ def generate_itempool(world, player: int):
world.escape_assist[player].append('bombs')
for (location, item) in placed_items.items():
world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
world.get_location(location, player).event = True
world.get_location(location, player).locked = True
world.get_location(location, player).place_locked_item(ItemFactory(item, player))
items = ItemFactory(pool, player)

77
worlds/alttp/Options.py Normal file
View File

@@ -0,0 +1,77 @@
import typing
from Options import Choice, Range, Option
class Logic(Choice):
option_no_glitches = 0
option_minor_glitches = 1
option_overworld_glitches = 2
option_hybrid_major_glitches = 3
option_no_logic = 4
alias_owg = 2
alias_hmg = 3
class Objective(Choice):
option_crystals = 0
# option_pendants = 1
option_triforce_pieces = 2
option_pedestal = 3
option_bingo = 4
class Goal(Choice):
option_kill_ganon = 0
option_kill_ganon_and_gt_agahnim = 1
option_hand_in = 2
class Crystals(Range):
range_start = 0
range_end = 7
class CrystalsTower(Crystals):
default = 7
class CrystalsGanon(Crystals):
default = 7
class TriforcePieces(Range):
default = 30
range_start = 1
range_end = 90
class ShopItemSlots(Range):
range_start = 0
range_end = 30
class WorldState(Choice):
option_standard = 1
option_open = 0
option_inverted = 2
class Bosses(Choice):
option_vanilla = 0
option_simple = 1
option_full = 2
option_chaos = 3
option_singularity = 4
class Enemies(Choice):
option_vanilla = 0
option_shuffled = 1
option_chaos = 2
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"shop_item_slots": ShopItemSlots,
}

View File

@@ -751,18 +751,11 @@ 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: str) -> int:
def get_nonnative_item_sprite(item: str) -> int:
return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
# 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.slot_seeds[player]
@@ -1724,20 +1717,7 @@ def write_custom_shops(rom, world, player):
if item is None:
break
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)
item_code = get_nonnative_item_sprite(item['item'])
else:
item_code = ItemFactory(item['item'], player).code
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:

View File

@@ -173,7 +173,10 @@ Blind_texts = [
"Do I like\ndrills? Just\na bit.",
"I'd shell out\ngood rupees\nfor a conch.",
"Current\naffairs are\nshocking!",
"Agriculture\nis a growing\nfield."
"Agriculture\nis a growing\nfield.",
"Did you hear\nabout the guy\nwhose whole\nleft side was\ncut off?\nHe's all right\nnow.",
"What do you\ncall a bee\nthat lives in\nAmerica?\nA USB.",
"Leather is\ngreat for\nsneaking\naround because\nit's made of\nhide.",
]
Ganon1_texts = [
"Start your day\nsmiling with a\ndelicious\nwhole grain\nbreakfast\ncreated for\nyour\nincredible\ninsides.",

View File

@@ -1,10 +1,70 @@
from typing import Optional
from BaseClasses import Location, Item
from BaseClasses import Location, Item, CollectionState
from ..AutoWorld import World
from .Options import alttp_options
class ALTTPWorld(World):
game: str = "A Link to the Past"
options = alttp_options
topology_present = True
def collect(self, state: CollectionState, item: Item) -> bool:
if item.name.startswith('Progressive '):
if 'Sword' in item.name:
if state.has('Golden Sword', item.player):
pass
elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
item.player].progressive_sword_limit >= 4:
state.prog_items['Golden Sword', item.player] += 1
return True
elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[
item.player].progressive_sword_limit >= 3:
state.prog_items['Tempered Sword', item.player] += 1
return True
elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
state.prog_items['Master Sword', item.player] += 1
return True
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
state.prog_items['Fighter Sword', item.player] += 1
return True
elif 'Glove' in item.name:
if state.has('Titans Mitts', item.player):
pass
elif state.has('Power Glove', item.player):
state.prog_items['Titans Mitts', item.player] += 1
return True
else:
state.prog_items['Power Glove', item.player] += 1
return True
elif 'Shield' in item.name:
if state.has('Mirror Shield', item.player):
pass
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
state.prog_items['Mirror Shield', item.player] += 1
return True
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
state.prog_items['Red Shield', item.player] += 1
return True
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
state.prog_items['Blue Shield', item.player] += 1
return True
elif 'Bow' in item.name:
if state.has('Silver', item.player):
pass
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2:
state.prog_items['Silver Bow', item.player] += 1
return True
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
state.prog_items['Bow', item.player] += 1
return True
elif item.advancement or item.smallkey or item.bigkey:
state.prog_items[item.name, item.player] += 1
return True
return False
def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
class ALttPLocation(Location):

View File

@@ -9,13 +9,15 @@ import json
import jinja2
import Utils
import shutil
import Options
from . import Options
from BaseClasses import MultiWorld
from .Technologies import tech_table, rocket_recipes, recipes
from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist, progressive_technology_table, \
base_tech_table, tech_to_progressive_lookup, progressive_tech_table
template_env: Optional[jinja2.Environment] = None
template: Optional[jinja2.Template] = None
data_template: Optional[jinja2.Template] = None
data_final_template: Optional[jinja2.Template] = None
locale_template: Optional[jinja2.Template] = None
control_template: Optional[jinja2.Template] = None
@@ -41,57 +43,65 @@ recipe_time_scales = {
Options.RecipeTime.option_vanilla: None
}
def generate_mod(world: MultiWorld, player: int):
global template, locale_template, control_template
def generate_mod(world):
player = world.player
multiworld = world.world
global data_final_template, locale_template, control_template, data_template
with template_load_lock:
if not template:
if not data_final_template:
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
template_env: Optional[jinja2.Environment] = \
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
template = template_env.get_template("data-final-fixes.lua")
data_template = template_env.get_template("data.lua")
data_final_template = template_env.get_template("data-final-fixes.lua")
locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua")
# get data for templates
player_names = {x: world.player_names[x][0] for x in world.player_ids}
player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids}
locations = []
for location in world.get_filled_locations(player):
for location in multiworld.get_filled_locations(player):
if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}"
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}"
tech_cost_scale = {0: 0.1,
1: 0.25,
2: 0.5,
3: 1,
4: 2,
5: 5,
6: 10}[world.tech_cost[player].value]
6: 10}[multiworld.tech_cost[player].value]
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale, "custom_technologies": world.worlds[player].custom_technologies,
"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], "seed_name": world.seed_name,
"starting_items": world.starting_items[player], "recipes": recipes,
"random": world.slot_seeds[player],
"recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]}
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
"slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name,
"starting_items": multiworld.starting_items[player], "recipes": recipes,
"random": multiworld.slot_seeds[player], "static_nodes": multiworld.worlds[player].static_nodes,
"recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value],
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
"progressive_technology_table": {tech.name : tech.progressive for tech in
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes}
for factorio_option in Options.factorio_options:
template_data[factorio_option] = getattr(world, factorio_option)[player].value
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
control_code = control_template.render(**template_data)
data_final_fixes_code = template.render(**template_data)
data_template_code = data_template.render(**template_data)
data_final_fixes_code = data_final_template.render(**template_data)
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.lua"), "wt") as f:
f.write(data_template_code)
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
f.write(data_final_fixes_code)
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
f.write(control_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)

123
worlds/factorio/Options.py Normal file
View File

@@ -0,0 +1,123 @@
import typing
from Options import Choice, OptionDict, Option, DefaultOnToggle
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} - \
{"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense
@classmethod
def get_ordered_science_packs(cls):
return [option.replace("_", "-") for option, value in sorted(cls.options.items(), key=lambda pair: pair[1])]
def get_max_pack(self):
return self.get_ordered_science_packs()[self.value].replace("_", "-")
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_large_diamonds = 3
option_small_pyramids = 4
option_medium_pyramids = 5
option_large_pyramids = 6
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_funnels = 4
alias_pyramid = 6
alias_funnel = 9
default = 0
class TechTreeInformation(Choice):
option_none = 0
option_advancement = 1
option_full = 2
default = 2
class RecipeTime(Choice):
option_vanilla = 0
option_fast = 1
option_normal = 2
option_slow = 4
option_chaos = 5
# TODO: implement random
class Progressive(Choice):
option_off = 0
option_random = 1
option_on = 2
default = 2
def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_random else int(self.value)
class RecipeIngredients(Choice):
option_rocket = 0
option_science_pack = 1
class FactorioStartItems(OptionDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19}
class FactorioWorldGen(OptionDict):
default = {"terrain_segmentation": 0.5, "water": 1.5,
"autoplace_controls": {"coal": {"frequency": 1, "size": 3, "richness": 6},
"copper-ore": {"frequency": 1, "size": 3, "richness": 6},
"crude-oil": {"frequency": 1, "size": 3, "richness": 6},
"enemy-base": {"frequency": 1, "size": 1, "richness": 1},
"iron-ore": {"frequency": 1, "size": 3, "richness": 6},
"stone": {"frequency": 1, "size": 3, "richness": 6},
"trees": {"frequency": 1, "size": 1, "richness": 1},
"uranium-ore": {"frequency": 1, "size": 3, "richness": 6}},
"starting_area": 1, "peaceful_mode": False,
"cliff_settings": {"name": "cliff", "cliff_elevation_0": 10, "cliff_elevation_interval": 40,
"richness": 1}}
factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack,
"tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost,
"free_samples": FreeSamples,
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"imported_blueprints": DefaultOnToggle,
"world_gen": FactorioWorldGen,
"progressive": DefaultOnToggle
}

View File

@@ -1,7 +1,6 @@
from typing import Dict, List, Set
from BaseClasses import MultiWorld
from Options import TechTreeLayout
from worlds.factorio.Options import TechTreeLayout
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
TechTreeLayout.option_medium_funnels: 4,
@@ -11,6 +10,7 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6,
TechTreeLayout.option_medium_funnels: 10,
TechTreeLayout.option_large_funnels: 15}
def get_shapes(factorio_world) -> Dict[str, List[str]]:
world = factorio_world.world
player = factorio_world.player

View File

@@ -1,14 +1,17 @@
from __future__ import annotations
# Factorio technologies are imported from a .json document in /data
from typing import Dict, Set, FrozenSet
from typing import Dict, Set, FrozenSet, Tuple
from collections import Counter, defaultdict
import os
import json
import string
import Options
import Utils
import logging
import functools
from . import Options
factorio_id = 2 ** 17
source_folder = Utils.local_path("data", "factorio")
@@ -35,10 +38,11 @@ class FactorioElement():
class Technology(FactorioElement): # maybe make subclass of Location?
def __init__(self, name, ingredients, factorio_id):
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = ()):
self.name = name
self.factorio_id = factorio_id
self.ingredients = ingredients
self.progressive = progressive
def build_rule(self, player: int):
logging.debug(f"Building rules for {self.name}")
@@ -74,7 +78,12 @@ class CustomTechnology(Technology):
class Recipe(FactorioElement):
def __init__(self, name, category, ingredients, products):
name: str
category: str
ingredients: Dict[str, int]
products: Dict[str, int]
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int]):
self.name = name
self.category = category
self.ingredients = ingredients
@@ -84,25 +93,47 @@ class Recipe(FactorioElement):
return f"{self.__class__.__name__}({self.name})"
@property
def crafting_machines(self) -> Set[Machine]:
"""crafting machines able to run this recipe"""
return machines_per_category[self.category]
def crafting_machine(self) -> str:
"""cheapest crafting machine name able to run this recipe"""
return machine_per_category[self.category]
@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, ())}
@property
def recursive_unlocking_technologies(self) -> Set[Technology]:
base = {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
for ingredient in self.ingredients:
base |= required_technologies[ingredient]
return base
@property
def rel_cost(self) -> float:
ingredients = sum(self.ingredients.values())
return min(ingredients/amount for product, amount in self.products.items())
@property
def base_cost(self) -> Dict[str, int]:
ingredients = Counter()
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
for recipe in all_product_sources[ingredient]:
ingredients.update({name: amount*cost/recipe.products[ingredient] for name, amount in recipe.base_cost.items()})
else:
ingredients[ingredient] += cost
return ingredients
class Machine(FactorioElement):
def __init__(self, name, categories):
self.name: str = name
self.categories: set = categories
# recipes and technologies can share names in Factorio
for technology_name in sorted(raw):
data = raw[technology_name]
factorio_id += 1
current_ingredients = set(data["ingredients"])
technology = Technology(technology_name, current_ingredients, factorio_id)
factorio_id += 1
@@ -116,16 +147,22 @@ for technology, data in raw.items():
recipe_sources.setdefault(recipe_name, set()).add(technology)
del (raw)
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
recipes = {}
all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
# add uranium mining to logic graph. TODO: add to automatic extractor for mod support
raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining"}
for recipe_name, recipe_data in raw_recipes.items():
# example:
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))
recipes[recipe_name] = Recipe
if recipe.products.isdisjoint(recipe.ingredients) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"], recipe_data["products"])
recipes[recipe_name] = recipe
if set(recipe.products).isdisjoint(
# prevents loop recipes like uranium centrifuging
set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \
not recipe_name.endswith("-reprocessing"):
for product_name in recipe.products:
all_product_sources.setdefault(product_name, set()).add(recipe)
@@ -137,6 +174,10 @@ for name, categories in raw_machines.items():
machine = Machine(name, set(categories))
machines[name] = machine
# add electric mining drill as a crafting machine to resolve uranium-ore
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"})
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
del (raw_machines)
# build requirements graph for all technology ingredients
@@ -147,22 +188,24 @@ for technology in technology_table.values():
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = set()
current_technologies |= recipe.unlocking_technologies
current_technologies = recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done,
unlock_func=unlock_just_tech)
return current_technologies
def unlock(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = set()
current_technologies |= recipe.unlocking_technologies
current_technologies = recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, unlock_func=unlock)
current_technologies |= required_category_technologies[recipe.category]
return current_technologies
def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[Technology]:
def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[
Technology]:
if _done:
if ingredient_name in _done:
return set()
@@ -180,58 +223,52 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f
return current_technologies
required_machine_technologies: Dict[str, FrozenSet[Technology]] = {}
for ingredient_name in machines:
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
logical_machines = {}
machine_tech_cost = {}
for machine in machines.values():
logically_useful = True
for pot_source_machine in machines.values():
if machine != pot_source_machine \
and machine.categories.issuperset(pot_source_machine.categories) \
and required_machine_technologies[machine.name].issuperset(
required_machine_technologies[pot_source_machine.name]):
logically_useful = False
break
if logically_useful:
logical_machines[machine.name] = machine
del(required_machine_technologies)
machines_per_category: Dict[str: Set[Machine]] = {}
for machine in logical_machines.values():
for category in machine.categories:
machines_per_category.setdefault(category, set()).add(machine)
current_cost, current_machine = machine_tech_cost.get(category, (10000, "character"))
machine_cost = len(required_machine_technologies[machine.name])
if machine_cost < current_cost:
machine_tech_cost[category] = machine_cost, machine.name
machine_per_category: Dict[str: str] = {}
for category, (cost, machine_name) in machine_tech_cost.items():
machine_per_category[category] = machine_name
del (machine_tech_cost)
# required technologies to be able to craft recipes from a certain category
required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {}
for category_name, cat_machines in machines_per_category.items():
for category_name, machine_name in machine_per_category.items():
techs = set()
for machine in cat_machines:
techs |= recursively_get_unlocking_technologies(machine.name)
techs |= recursively_get_unlocking_technologies(machine_name)
required_category_technologies[category_name] = frozenset(techs)
required_technologies: Dict[str, FrozenSet[Technology]] = {}
for ingredient_name in all_ingredient_names:
required_technologies[ingredient_name] = frozenset(
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name : frozenset(
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
advancement_technologies: Set[str] = set()
for technologies in required_technologies.values():
for ingredient_name in all_ingredient_names:
technologies = required_technologies[ingredient_name]
advancement_technologies |= {technology.name for technology in technologies}
@functools.lru_cache(10)
def get_rocket_requirements(ingredients: Set[str]) -> Set[str]:
def get_rocket_requirements(recipe: Recipe) -> Set[str]:
techs = recursively_get_unlocking_technologies("rocket-silo")
for ingredient in ingredients:
for ingredient in recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
return {tech.name for tech in techs}
free_sample_blacklist = all_ingredient_names | {"rocket-part"}
rocket_recipes = {
Options.MaxSciencePack.option_space_science_pack:
{"rocket-control-unit": 10, "low-density-structure": 10, "rocket-fuel": 10},
@@ -247,4 +284,136 @@ rocket_recipes = {
{"electronic-circuit": 10, "stone-brick": 10, "coal": 10},
Options.MaxSciencePack.option_automation_science_pack:
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
}
}
advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]}
# progressive technologies
# auto-progressive
progressive_rows = {}
progressive_incs = set()
for tech_name in tech_table:
if tech_name.endswith("-1"):
progressive_rows[tech_name] = []
elif tech_name[-2] == "-" and tech_name[-1] in string.digits:
progressive_incs.add(tech_name)
for root, progressive in progressive_rows.items():
seeking = root[:-1]+str(int(root[-1])+1)
while seeking in progressive_incs:
progressive.append(seeking)
progressive_incs.remove(seeking)
seeking = seeking[:-1]+str(int(seeking[-1])+1)
# make root entry the progressive name
for old_name in set(progressive_rows):
prog_name = "progressive-" + old_name.rsplit("-", 1)[0]
progressive_rows[prog_name] = tuple([old_name] + progressive_rows[old_name])
del(progressive_rows[old_name])
# no -1 start
base_starts = set()
for remnant in progressive_incs:
if remnant[-1] == "2":
base_starts.add(remnant[:-2])
for root in base_starts:
seeking = root+"-2"
progressive = [root]
while seeking in progressive_incs:
progressive.append(seeking)
seeking = seeking[:-1]+str(int(seeking[-1])+1)
progressive_rows["progressive-"+root] = tuple(progressive)
# science packs
progressive_rows["progressive-science-pack"] = tuple(Options.MaxSciencePack.get_ordered_science_packs())[1:]
# manual progressive
progressive_rows["progressive-processing"] = (
"steel-processing",
"oil-processing", "sulfur-processing", "advanced-oil-processing", "coal-liquefaction",
"uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing")
progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb")
progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron")
progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", "automated-rail-transportation", "rail-signals")
progressive_rows["progressive-engine"] = ("engine", "electric-engine")
progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2")
progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment")
progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "energy-shield-mk2-equipment")
progressive_rows["progressive-wall"] = ("stone-wall", "gate")
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter")
base_tech_table = tech_table.copy() # without progressive techs
base_technology_table = technology_table.copy()
progressive_tech_table: Dict[str, int] = {}
progressive_technology_table: Dict[str, Technology] = {}
for root in sorted(progressive_rows):
progressive = progressive_rows[root]
assert all(tech in tech_table for tech in progressive)
factorio_id += 1
progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id,
progressive)
progressive_tech_table[root] = progressive_technology.factorio_id
progressive_technology_table[root] = progressive_technology
if any(tech in advancement_technologies for tech in progressive):
advancement_technologies.add(root)
tech_to_progressive_lookup: Dict[str, str] = {}
for technology in progressive_technology_table.values():
for progressive in technology.progressive:
tech_to_progressive_lookup[progressive] = technology.name
tech_table.update(progressive_tech_table)
technology_table.update(progressive_technology_table)
# techs that are never progressive
common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items()
if tech_name not in progressive_tech_table}
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
rel_cost = {
"wood" : 10000,
"iron-ore": 1,
"copper-ore": 1,
"stone": 1,
"crude-oil": 0.5,
"water": 0.001,
"coal": 1,
"raw-fish": 1000,
"steam": 0.01,
"used-up-uranium-fuel-cell": 1000
}
# forbid liquids for now, TODO: allow a single liquid per assembler
blacklist = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas", "light-oil",
"heavy-oil", "lubricant", "steam"}
@Utils.cache_argsless
def get_science_pack_pools() -> Dict[str, Set[str]]:
def get_estimated_difficulty(recipe: Recipe):
base_ingredients = recipe.base_cost
cost = 0
for ingredient_name, amount in base_ingredients.items():
cost += rel_cost.get(ingredient_name, 1) * amount
return cost
science_pack_pools = {}
already_taken = blacklist.copy()
current_difficulty = 5
for science_pack in Options.MaxSciencePack.get_ordered_science_packs():
current = science_pack_pools[science_pack] = set()
for name, recipe in recipes.items():
if (science_pack != "automation-science-pack" or not recipe.recursive_unlocking_technologies) \
and get_estimated_difficulty(recipe) < current_difficulty:
current |= set(recipe.products)
current -= already_taken
already_taken |= current
current_difficulty *= 2
return science_pack_pools

View File

@@ -1,31 +1,41 @@
from ..AutoWorld import World
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes
from BaseClasses import Region, Entrance, Location, Item
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
get_science_pack_pools, Recipe, recipes, technology_table
from .Shapes import get_shapes
from .Mod import generate_mod
from .Options import factorio_options
class Factorio(World):
game: str = "Factorio"
static_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes = {}
additional_advancement_technologies = set()
def generate_basic(self):
victory_tech_names = get_rocket_requirements(
frozenset(rocket_recipes[self.world.max_science_pack[self.player].value]))
for tech_name, tech_id in base_tech_table.items():
if self.world.progressive and tech_name in tech_to_progressive_lookup:
item_name = tech_to_progressive_lookup[tech_name]
tech_id = progressive_tech_table[item_name]
else:
item_name = tech_name
for tech_name, tech_id in tech_table.items():
tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names,
tech_item = Item(item_name, item_name in advancement_technologies or
item_name in self.additional_advancement_technologies,
tech_id, self.player)
tech_item.game = "Factorio"
if tech_name in self.static_nodes:
self.world.get_location(tech_name, self.player).place_locked_item(tech_item)
else:
self.world.itempool.append(tech_item)
world_gen = self.world.world_gen[self.player].value
if world_gen.get("seed", None) is None: # allow seed 0
world_gen["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint
def generate_output(self):
generate_mod(self.world, self.player)
generate_output = generate_mod
def create_regions(self):
player = self.player
@@ -35,17 +45,20 @@ class Factorio(World):
nauvis = Region("Nauvis", None, "Nauvis", player)
nauvis.world = menu.world = self.world
for tech_name, tech_id in tech_table.items():
for tech_name, tech_id in base_tech_table.items():
tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech)
tech.game = "Factorio"
location = Location(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location)
location.game = "Factorio"
event = Item("Victory", True, None, player)
event.game = "Factorio"
self.world.push_item(location, event, False)
location.event = location.locked = True
for ingredient in all_ingredient_names:
for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
location = Location(player, f"Automate {ingredient}", None, nauvis)
location.game = "Factorio"
nauvis.locations.append(location)
event = Item(f"Automated {ingredient}", True, None, player)
self.world.push_item(location, event, False)
@@ -56,14 +69,25 @@ class Factorio(World):
def set_rules(self):
world = self.world
player = self.player
self.custom_technologies = set_custom_technologies(self.world, self.player)
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
shapes = get_shapes(self)
if world.logic[player] != 'nologic':
from worlds.generic import Rules
for ingredient in all_ingredient_names:
for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player)
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
if self.world.recipe_ingredients[self.player]:
custom_recipe = self.custom_recipes[ingredient]
location.access_rule = lambda state, ingredient=ingredient, custom_recipe = custom_recipe: \
(ingredient not in technology_table or state.has(ingredient, player)) and \
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
for technology in required_technologies[sub_ingredient])
else:
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for tech_name, technology in self.custom_technologies.items():
location = world.get_location(tech_name, player)
Rules.set_rule(location, technology.build_rule(player))
@@ -72,17 +96,64 @@ class Factorio(World):
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))
# get all science pack technologies (but not the ability to craft them)
victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value]))
victory_tech_names = get_rocket_requirements(self.custom_recipes["rocket-part"])
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
world.completion_condition[player] = lambda state: state.has('Victory', player)
def set_custom_technologies(world: MultiWorld, player: int):
custom_technologies = {}
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 collect(self, state, item) -> bool:
if item.advancement and item.name in progressive_technology_table:
prog_table = progressive_technology_table[item.name].progressive
for item_name in prog_table:
if not state.has(item_name, item.player):
state.prog_items[item_name, item.player] += 1
return True
return super(Factorio, self).collect(state, item)
def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(Factorio, self).get_required_client_version())
options = factorio_options
def set_custom_technologies(self):
custom_technologies = {}
allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs()
for technology_name, technology in base_technology_table.items():
custom_technologies[technology_name] = technology.get_custom(self.world, allowed_packs, self.player)
return custom_technologies
def set_custom_recipes(self):
original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
self.world.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x] : 10 for x in range(3)},
original_rocket_part.products)}
self.additional_advancement_technologies = {tech.name for tech in
self.custom_recipes["rocket-part"].recursive_unlocking_technologies}
if self.world.recipe_ingredients[self.player]:
valid_pool = []
for pack in self.world.max_science_pack[self.player].get_ordered_science_packs():
valid_pool += sorted(science_pack_pools[pack])
self.world.random.shuffle(valid_pool)
if pack in recipes: # skips over space science pack
original = recipes[pack]
new_ingredients = {}
for _ in original.ingredients:
new_ingredients[valid_pool.pop()] = 1
new_recipe = Recipe(pack, original.category, new_ingredients, original.products)
self.additional_advancement_technologies |= {tech.name for tech in
new_recipe.recursive_unlocking_technologies}
self.custom_recipes[pack] = new_recipe
# handle marking progressive techs as advancement
prog_add = set()
for tech in self.additional_advancement_technologies:
if tech in tech_to_progressive_lookup:
prog_add.add(tech_to_progressive_lookup[tech])
self.additional_advancement_technologies |= prog_add

39
worlds/hk/Options.py Normal file
View File

@@ -0,0 +1,39 @@
import typing
from Options import Option, DefaultOnToggle, Toggle
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {
"RandomizeDreamers": DefaultOnToggle,
"RandomizeSkills": DefaultOnToggle,
"RandomizeCharms": DefaultOnToggle,
"RandomizeKeys": DefaultOnToggle,
"RandomizeGeoChests": Toggle,
"RandomizeMaskShards": DefaultOnToggle,
"RandomizeVesselFragments": DefaultOnToggle,
"RandomizeCharmNotches": Toggle,
"RandomizePaleOre": DefaultOnToggle,
"RandomizeRancidEggs": Toggle,
"RandomizeRelics": DefaultOnToggle,
"RandomizeMaps": Toggle,
"RandomizeStags": Toggle,
"RandomizeGrubs": Toggle,
"RandomizeWhisperingRoots": Toggle,
"RandomizeRocks": Toggle,
"RandomizeSoulTotems": Toggle,
"RandomizePalaceTotems": Toggle,
"RandomizeLoreTablets": Toggle,
"RandomizeLifebloodCocoons": Toggle,
"RandomizeFlames": Toggle
}
hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
"MILDSKIPS": Toggle,
"SPICYSKIPS": Toggle,
"FIREBALLSKIPS": Toggle,
"ACIDSKIPS": Toggle,
"SPIKETUNNELS": Toggle,
"DARKROOMS": Toggle,
"CURSED": Toggle,
"SHADESKIPS": Toggle,
}
hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options,
**hollow_knight_skip_options}

View File

@@ -6,12 +6,83 @@ from .Locations import lookup_name_to_id
from .Items import item_table
from .Regions import create_regions
from .Rules import set_rules
from .Options import hollow_knight_options
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from ..AutoWorld import World
class HKWorld(World):
game: str = "Hollow Knight"
options = hollow_knight_options
def generate_basic(self):
# Link regions
self.world.get_entrance('Hollow Nest S&Q', self.player).connect(self.world.get_region('Hollow Nest', self.player))
# Generate item pool
pool = []
for item_name, item_data in item_table.items():
item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=self.player)
if item_data.type == "Event":
event_location = self.world.get_location(item_name, self.player)
self.world.push_item(event_location, item, collect=False)
event_location.event = True
event_location.locked = True
if item.name == "King's_Pass":
self.world.push_precollected(item)
elif item_data.type == "Cursed":
if self.world.CURSED[self.player]:
pool.append(item)
else:
# fill Focus Location with Focus and add it to start inventory as well.
event_location = self.world.get_location(item_name, self.player)
self.world.push_item(event_location, item)
event_location.event = True
event_location.locked = True
elif item_data.type == "Fake":
pass
elif item_data.type in not_shufflable_types:
location = self.world.get_location(item_name, self.player)
self.world.push_item(location, item, collect=False)
location.event = item.advancement
location.locked = True
else:
target = option_to_type_lookup[item.type]
shuffle_it = getattr(self.world, target)
if shuffle_it[self.player]:
pool.append(item)
else:
location = self.world.get_location(item_name, self.player)
self.world.push_item(location, item, collect=False)
location.event = item.advancement
location.locked = True
logger.debug(f"Placed {item_name} to vanilla for player {self.player}")
self.world.itempool += pool
def set_rules(self):
set_rules(self.world, self.player)
def create_regions(self):
create_regions(self.world, self.player)
def generate_output(self):
pass # Hollow Knight needs no output files
def fill_slot_data(self):
slot_data = {}
for option_name in self.options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = int(option.value)
return slot_data
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player)
@@ -43,16 +114,6 @@ class HKItem(Item):
self.type = type
def gen_hollow(world: MultiWorld, player: int):
link_regions(world, player)
gen_items(world, player)
set_rules(world, player)
def link_regions(world: MultiWorld, player: int):
world.get_entrance('Hollow Nest S&Q', player).connect(world.get_region('Hollow Nest', player))
not_shufflable_types = {"Essence_Boss"}
option_to_type_lookup = {
@@ -75,49 +136,6 @@ option_to_type_lookup = {
"Vessel": "RandomizeVesselFragments",
}
def gen_items(world: MultiWorld, player: int):
pool = []
for item_name, item_data in item_table.items():
item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=player)
if item_data.type == "Event":
event_location = world.get_location(item_name, player)
world.push_item(event_location, item, collect=False)
event_location.event = True
event_location.locked = True
if item.name == "King's_Pass":
world.push_precollected(item)
elif item_data.type == "Cursed":
if world.CURSED[player]:
pool.append(item)
else:
# fill Focus Location with Focus and add it to start inventory as well.
event_location = world.get_location(item_name, player)
world.push_item(event_location, item)
event_location.event = True
event_location.locked = True
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, collect=False)
location.event = item.advancement
location.locked = True
else:
target = option_to_type_lookup[item.type]
shuffle_it = getattr(world, target)
if shuffle_it[player]:
pool.append(item)
else:
location = world.get_location(item_name, player)
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}")
world.itempool += pool

View File

@@ -1,4 +1,4 @@
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from BaseClasses import Item
import typing
class ItemData(typing.NamedTuple):
@@ -46,6 +46,7 @@ item_table = {
"8 Gold Ore": ItemData(45032, False),
"Rotten Flesh": ItemData(45033, False),
"Single Arrow": ItemData(45034, False),
"Bee Trap (Minecraft)": ItemData(45100, False),
"Victory": ItemData(0, True)
}
@@ -68,7 +69,8 @@ item_frequencies = {
"16 Porkchops": 8,
"8 Gold Ore": 4,
"Rotten Flesh": 4,
"Single Arrow": 0
"Single Arrow": 0,
"Bee Trap (Minecraft)": 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

@@ -1,4 +1,4 @@
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from BaseClasses import Location
import typing
class AdvData(typing.NamedTuple):
@@ -114,6 +114,7 @@ exclusion_table = {
"Two by Two": "100 XP",
"Two Birds, One Arrow": "50 XP",
"Arbalistic": "100 XP",
"Monsters Hunted": "100 XP",
"Beaconator": "50 XP",
"A Balanced Diet": "100 XP",
"Uneasy Alliance": "100 XP",

View File

@@ -0,0 +1,26 @@
import typing
from Options import Choice, Option, Toggle, Range
class AdvancementGoal(Range):
range_start = 0
range_end = 87
default = 50
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,
"bee_traps": Toggle
}

View File

@@ -1,73 +1,44 @@
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]
def link_minecraft_structures(world, player):
# Link mandatory connections first
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']
exits_spoiler = exits[:] # copy the original order for the spoiler log
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
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_names[player]})")
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):
try:
assert exit in exits
assert struct in structs
except AssertionError as e:
raise Exception(f"Invalid connection: {exit} => {struct} for player {player}")
pairs[exit] = struct
exits.remove(exit)
structs.remove(struct)
if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])):
pairs[exit] = struct
exits.remove(exit)
structs.remove(struct)
else:
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_names[player]})")
# Plando stuff. Remove any utilized exits/structs from the lists.
# Raise error if trying to put Nether Fortress in the End.
# Connect plando structures first
if world.plando_connections[player]:
for connection in world.plando_connections[player]:
try:
if connection.entrance == 'The End Structure' and connection.exit == 'Nether Fortress':
raise Exception(f"Cannot place Nether Fortress in the End for player {player}")
set_pair(connection.entrance, connection.exit)
except Exception as e:
raise Exception(f"Could not connect using {connection}") from e
for conn in world.plando_connections[player]:
set_pair(conn.entrance, conn.exit)
# The algorithm tries to place the most restrictive structures first. This algorithm always works on the
# relatively small set of restrictions here, but does not work on all possible inputs with valid configurations.
if world.shuffle_structures[player]:
# Can't put Nether Fortress in the End
if 'The End Structure' in exits and 'Nether Fortress' in structs:
structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, [])))
for struct 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: # should only happen if structs is emptied by plando
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[:]):
exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
except IndexError:
raise Exception(f"No valid structure placements remaining for player {player} ({world.player_names[player]})")
set_pair(exit, struct)
else: # write remaining default connections
for (exit, struct) in default_connections:
@@ -77,13 +48,15 @@ def link_minecraft_structures(world: MultiWorld, player: int):
# 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
except AssertionError:
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_names[player]})")
for exit in exits_spoiler:
world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player))
if world.shuffle_structures[player] or world.plando_connections[player]:
world.spoiler.set_entrance(exit, pairs[exit], 'entrance', player)
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 = [
@@ -112,3 +85,9 @@ default_connections = {
('Nether Structure 2', 'Bastion Remnant'),
('The End Structure', 'End City')
}
# Structure: illegal locations
illegal_connections = {
'Nether Fortress': ['The End Structure']
}

View File

@@ -1,35 +1,31 @@
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
from BaseClasses import MultiWorld
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
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()]
# 92 total advancements. Goal is to complete X advancements and then Free the End.
# There are 5 advancements which cannot be included for dragon spawning (4 postgame, Free the End)
# Hence the true maximum is (92 - 5) = 87
goal = int(world.advancement_goal[player].value)
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':
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))
(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))
@@ -45,15 +41,15 @@ def set_rules(world: MultiWorld, player: int):
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) and \
((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike
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) and state.has('Ingot Crafting', player) and state.can_reach('The Nether', 'Region', 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 # Water Breathing
state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets
state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility
state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance
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("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))
@@ -64,14 +60,16 @@ def set_rules(world: MultiWorld, player: int):
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("How Did We Get Here?", player), lambda state: state.can_brew_potions(player) and
state.has_gold_ingots(player) and # Absorption
state.can_reach('End City', 'Region', player) and # Levitation
state.can_reach('The Nether', 'Region', player) and # 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("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)
@@ -88,7 +86,7 @@ def set_rules(world: MultiWorld, player: int):
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("The End... Again...", player), lambda state: can_complete(state))
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))
@@ -98,7 +96,7 @@ def set_rules(world: MultiWorld, player: int):
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("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))
@@ -110,12 +108,12 @@ def set_rules(world: MultiWorld, player: int):
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("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("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))
@@ -123,26 +121,28 @@ def set_rules(world: MultiWorld, player: int):
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) and state.has('Fishing Rod', 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("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("Campfire", player) and 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("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("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) and state.has('Progressive Tools', player, 2)) # powered rails
set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails
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) or state.complete_raid(player)) and state.has("Fishing Rod", player) and state.can_adventure(player))
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit
set_rule(world.get_location("When Pigs Fly", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and
state.has("Fishing Rod", player) and state.can_adventure(player))
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and
(state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood 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

@@ -1,81 +1,95 @@
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 .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table
from .Regions import mc_regions, link_minecraft_structures
from .Rules import set_rules
from BaseClasses import MultiWorld
from Options import minecraft_options
from BaseClasses import Region, Entrance
from .Options import minecraft_options
from ..AutoWorld import World
client_version = (0, 4)
class MinecraftWorld(World):
game: str = "Minecraft"
options = minecraft_options
topology_present = True
def _get_mc_data(self):
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2",
"The End Structure"]
return {
'world_seed': self.world.slot_seeds[self.player].getrandbits(32),
# consistent and doesn't interfere with other generation
'seed_name': self.world.seed_name,
'player_name': self.world.get_player_names(self.player),
'player_id': self.player,
'client_version': client_version,
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits}
}
client_version = (0, 3)
def generate_basic(self):
link_minecraft_structures(self.world, self.player)
pool = []
pool_counts = item_frequencies.copy()
if getattr(self.world, "bee_traps")[self.player]:
pool_counts.update({"Rotten Flesh": 0, "Bee Trap (Minecraft)": 4})
for item_name, item_data in item_table.items():
for count in range(pool_counts.get(item_name, 1)):
pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, self.player))
prefill_pool = {}
prefill_pool.update(events_table)
exclusion_pools = ['hard', 'insane', 'postgame']
for key in exclusion_pools:
if not getattr(self.world, f"include_{key}_advancements")[self.player]:
prefill_pool.update(exclusion_table[key])
for loc_name, item_name in prefill_pool.items():
item_data = item_table[item_name]
location = self.world.get_location(loc_name, self.player)
item = MinecraftItem(item_name, item_data.progression, item_data.code, self.player)
self.world.push_item(location, item, collect=False)
pool.remove(item)
location.event = item_data.progression
location.locked = True
self.world.itempool += pool
def get_mc_data(world: MultiWorld, player: int):
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2",
"The End Structure"]
return {
'world_seed': world.slot_seeds[player].getrandbits(32),
# consistent and doesn't interfere with other generation
'seed_name': world.seed_name,
'player_name': world.get_player_names(player),
'player_id': player,
'client_version': client_version,
'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits}
}
def set_rules(self):
set_rules(self.world, self.player)
def generate_mc_data(world: MultiWorld, player: int):
import base64, json
from Utils import output_path
def create_regions(self):
def MCRegion(region_name: str, exits=[]):
ret = Region(region_name, None, region_name, self.player)
ret.world = self.world
ret.locations = [ MinecraftAdvancement(self.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(self.player, exit, ret))
return ret
data = get_mc_data(world, player)
filename = f"AP_{world.seed_name}_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')))
self.world.regions += [MCRegion(*r) for r in mc_regions]
def fill_minecraft_slot_data(world: MultiWorld, player: int):
slot_data = get_mc_data(world, player)
for option_name in minecraft_options:
option = getattr(world, option_name)[player]
slot_data[option_name] = int(option.value)
return slot_data
def generate_output(self):
import json
from base64 import b64encode
from Utils import output_path
data = self._get_mc_data()
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_names(self.player)}.apmc"
with open(output_path(filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
# 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)
def fill_slot_data(self):
slot_data = self._get_mc_data()
for option_name in minecraft_options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = int(option.value)
return slot_data