implement Factorio options max_science_pack and tech_cost

also give warnings about deprecated LttP options
also fix FactorioClient.py getting stuck if send an unknown item id
also fix !missing having an extra newline after each entry
also default to no webui
This commit is contained in:
Fabian Dill 2021-04-03 14:47:49 +02:00
parent d225eb9ca8
commit 91bcd59940
13 changed files with 177 additions and 42 deletions

View File

@ -137,11 +137,12 @@ async def factorio_server_watcher(ctx: FactorioContext):
if ctx.rcon_client:
while ctx.send_index < len(ctx.items_received):
item_id = ctx.items_received[ctx.send_index].item
item_name = lookup_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
response = ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
if response:
factorio_server_logger.info(response)
if item_id not in lookup_id_to_name:
logging.error(f"Unknown item ID: {item_id}")
else:
item_name = lookup_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis.")
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}')
ctx.send_index += 1
await asyncio.sleep(1)

View File

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

View File

@ -135,6 +135,8 @@ def main(args, seed=None):
import Options
for hk_option in Options.hollow_knight_options:
setattr(world, hk_option, getattr(args, hk_option, {}))
for factorio_option in Options.factorio_options:
setattr(world, factorio_option, getattr(args, factorio_option, {}))
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}

View File

@ -794,7 +794,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client)
if locations:
texts = [f'Missing: {get_item_name_from_id(location)}\n' for location in locations]
texts = [f'Missing: {get_item_name_from_id(location)}' for location in locations]
texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:

View File

@ -198,11 +198,15 @@ def main(args=None, callback=ERmain):
pre_rolled["original_seed_name"] = seedname
pre_rolled["pre_rolled"] = vars(settings).copy()
if "plando_items" in pre_rolled["pre_rolled"]:
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in pre_rolled["pre_rolled"]["plando_items"]]
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in
pre_rolled["pre_rolled"]["plando_items"]]
if "plando_connections" in pre_rolled["pre_rolled"]:
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in pre_rolled["pre_rolled"]["plando_connections"]]
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in
pre_rolled["pre_rolled"][
"plando_connections"]]
with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
with open(os.path.join(args.outputpath if args.outputpath else ".",
f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
yaml.dump(pre_rolled, f)
for k, v in vars(settings).items():
if v is not None:
@ -294,7 +298,8 @@ def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name] += 1
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
NUMBER=(name_counter[name] if name_counter[name] > 1 else ''),
NUMBER=(name_counter[name] if name_counter[
name] > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
return new_name.strip().replace(' ', '_')[:16]
@ -315,17 +320,42 @@ available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if leve
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'normal': 'normal',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
# remove sometime before 1.0.0, warn before
legacy_boss_shuffle_options = {
# legacy, will go away:
'simple': 'basic',
'random': 'full',
}
legacy_goals = {
'dungeons': 'bosses',
'fast_ganon': 'crystals',
}
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}')
new_options = set(new_weights) - set(weights)
@ -337,6 +367,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
f'This is probably in error.')
return weights
def roll_linked_options(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]:
@ -349,7 +380,8 @@ def roll_linked_options(weights: dict) -> dict:
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", option_set["name"])
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
option_set["name"])
weights["rom"] = rom_weights
else:
logging.debug(f"linked option {option_set['name']} skipped.")
@ -358,6 +390,7 @@ def roll_linked_options(weights: dict) -> dict:
f"Please fix your linked option.") from e
return weights
def roll_triggers(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
@ -375,7 +408,8 @@ def roll_triggers(weights: dict) -> dict:
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom", option_set["option_name"])
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
option_set["option_name"])
weights["rom"] = rom_weights
weights[key] = result
except Exception as e:
@ -385,6 +419,11 @@ def roll_triggers(weights: dict) -> dict:
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in legacy_boss_shuffle_options:
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
f"please use {new_boss_shuffle} instead")
return new_boss_shuffle
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
@ -392,6 +431,10 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in legacy_boss_shuffle_options:
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss} is deprecated, "
f"please use {remainder_shuffle} instead")
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
@ -419,7 +462,7 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses", ))):
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
if "pre_rolled" in weights:
pre_rolled = weights["pre_rolled"]
@ -435,7 +478,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
if "plando_connections" in pre_rolled:
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
connection["exit"],
connection["direction"]) for connection in pre_rolled["plando_connections"]]
connection["direction"]) for connection in
pre_rolled["plando_connections"]]
if "connections" not in plando_options and pre_rolled["plando_connections"]:
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
@ -486,11 +530,16 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
for option_name, option in Options.hollow_knight_options.items():
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
elif ret.game == "Factorio":
pass
for option_name, option in Options.factorio_options.items():
if option_name in weights:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else:
setattr(ret, option_name, option.default)
else:
raise Exception(f"Unsupported game {ret.game}")
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
@ -533,17 +582,11 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights, 'ganon')
ret.goal = {'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt'
}[goal]
if goal in legacy_goals:
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
goal = legacy_goals[goal]
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
@ -602,6 +645,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
@ -772,5 +816,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.quickswap = True
ret.sprite = "Link"
if __name__ == '__main__':
main()

View File

@ -23,6 +23,7 @@ class AssembleOptions(type):
class Option(metaclass=AssembleOptions):
value: int
name_lookup: typing.Dict[int, str]
default = 0
def __repr__(self):
return f"{self.__class__.__name__}({self.get_option_name()})"
@ -47,6 +48,7 @@ class Option(metaclass=AssembleOptions):
class Toggle(Option):
option_false = 0
option_true = 1
default = 0
def __init__(self, value: int):
self.value = value
@ -86,6 +88,7 @@ class Toggle(Option):
def get_option_name(self):
return bool(self.value)
class Choice(Option):
def __init__(self, value: int):
self.value: int = value
@ -233,6 +236,42 @@ hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options, **hollow_knight_skip_options}
class MaxSciencePack(Choice):
option_automation_science_pack = 0
option_logistic_science_pack = 1
option_military_science_pack = 2
option_chemical_science_pack = 3
option_production_science_pack = 4
option_utility_science_pack = 5
option_space_science_pack = 6
default = 6
def get_allowed_packs(self):
return {option.replace("_", "-") for option, value in self.options.items()
if value <= self.value}
class TechCost(Choice):
option_very_easy = 0
option_easy = 1
option_kind = 2
option_normal = 3
option_hard = 4
option_very_hard = 5
option_insane = 6
default = 3
class TechTreeLayout(Choice):
option_single = 0
default = 0
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
"tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost}
if __name__ == "__main__":
import argparse

View File

@ -2,11 +2,28 @@
local technologies = data.raw["technology"]
local original_tech
local new_tree_copy
allowed_ingredients = {}
{%- for ingredient in allowed_science_packs %}
allowed_ingredients["{{ingredient}}"]= 1
{% endfor %}
local template_tech = table.deepcopy(technologies["automation"])
{#- ensure the copy unlocks nothing #}
template_tech.unlocks = {}
template_tech.upgrade = false
template_tech.effects = {}
template_tech.prerequisites = {}
function filter_ingredients(ingredients)
local new_ingredient_list = {}
for _, ingredient_table in pairs(ingredients) do
if allowed_ingredients[ingredient_table[1]] then -- name of ingredient_table
table.insert(new_ingredient_list, ingredient_table)
end
end
return new_ingredient_list
end
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player in locations %}
original_tech = technologies["{{original_tech_name}}"]
@ -17,6 +34,12 @@ new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
original_tech.enabled = false
{#- copy original tech costs #}
new_tree_copy.unit = table.deepcopy(original_tech.unit)
new_tree_copy.unit.ingredients = filter_ingredients(new_tree_copy.unit.ingredients)
{% if tech_cost != 1 %}
if new_tree_copy.unit.count then
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost }}))
end
{% endif %}
{% if item_name in tech_table %}
{#- copy Factorio Technology Icon #}
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)

View File

@ -35,6 +35,25 @@ accessibility:
progression_balancing:
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
# Factorio options:
tech_tree_layout:
single: 1
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0
military_science_pack: 0
chemical_science_pack: 0
production_science_pack: 0
utility_science_pack: 0
space_science_pack: 1
tech_cost:
very_easy : 0
easy : 0
kind : 0
normal : 1
hard : 0
very_hard : 0
insane : 0
# A Link to the Past options:
### Logic Section ###
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version

View File

@ -241,7 +241,7 @@ def place_bosses(world, player: int):
if shuffle_mode == "none":
return # vanilla bosses come pre-placed
if shuffle_mode in ["basic", "normal"]:
if shuffle_mode in ["basic", "full"]:
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random

View File

@ -1651,7 +1651,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
item_code = 0x21
item_code = 0x09 # Hammer
else:
item_code = ItemFactory(item['item'], player).code
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:

View File

@ -35,11 +35,19 @@ def generate_mod(world: MultiWorld, player: int):
player_names = {x: world.player_names[x][0] for x in world.player_ids}
locations = []
for location in world.get_filled_locations(player):
if not location.name.startswith("recipe-"): # introduce this a new location property?
if not location.name.startswith("recipe-"): # introduce this as a new location property?
locations.append((location.name, location.item.name, location.item.player))
mod_name = f"archipelago-client-{world.seed}-{player}"
tech_cost = {0: 0.1,
1: 0.25,
2: 0.5,
3: 1,
4: 2,
5: 5,
6: 10}[world.tech_cost[player].value]
template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table,
"mod_name": mod_name}
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
"tech_cost": tech_cost}
mod_code = template.render(**template_data)

View File

@ -29,10 +29,8 @@ for technology in sorted(raw):
requirements[technology] = set(data["requires"])
current_ingredients = set(data["ingredients"])-starting_ingredient_recipes
if current_ingredients:
all_ingredients |= current_ingredients
current_ingredients = {"recipe-"+ingredient for ingredient in current_ingredients}
ingredients[technology] = current_ingredients
ingredients[technology] = {"recipe-"+ingredient for ingredient in current_ingredients}
recipe_sources = {}
@ -41,6 +39,6 @@ for technology, data in raw.items():
for recipe in recipe_source:
recipe_sources["recipe-"+recipe] = technology
all_ingredients = {"recipe-"+ingredient for ingredient in all_ingredients}
all_ingredients_recipe = {"recipe-"+ingredient for ingredient in all_ingredients}
del(raw)
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}

View File

@ -2,7 +2,7 @@ import logging
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from .Technologies import tech_table, requirements, ingredients, all_ingredients, recipe_sources
from .Technologies import tech_table, requirements, ingredients, all_ingredients, recipe_sources, all_ingredients_recipe
static_nodes = {"automation", "logistics"}
@ -30,7 +30,7 @@ def factorio_create_regions(world: MultiWorld, player: int):
tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech)
tech.game = "Factorio"
for ingredient in all_ingredients: # register science packs as events
for ingredient in all_ingredients_recipe: # register science packs as events
ingredient_location = Location(player, ingredient, 0, nauvis)
ingredient_location.item = Item(ingredient, True, 0, player)
ingredient_location.event = ingredient_location.locked = True
@ -56,4 +56,4 @@ def set_rules(world: MultiWorld, player: int):
world.completion_condition[player] = lambda state: all(state.has(ingredient, player)
for ingredient in all_ingredients)
for ingredient in all_ingredients_recipe)