Initial Commit
This commit is contained in:
commit
fa38469735
30 files changed
+6902
No files matched your search
+636
@@ -0,0 +1,636 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable, Optional, ClassVar, Counter, Any
|
||||
import webbrowser
|
||||
|
||||
import Utils
|
||||
from worlds.generic.Rules import forbid_items_for_player
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, components, Type, launch, icon_paths
|
||||
|
||||
from .Data import item_table, location_table, event_table, category_table
|
||||
from .Game import game_name, filler_item_name, starting_items, unused_goals_are_locations, glitches_item_name
|
||||
from .Meta import world_description, world_webworld
|
||||
from .Locations import location_id_to_name, location_name_to_id, location_name_to_location, location_name_groups, victory_names, event_name_to_event, event_name_groups
|
||||
from .Items import item_id_to_name, item_name_to_id, item_name_to_item, item_name_groups
|
||||
from .DataValidation import runGenerationDataValidation, runPreFillDataValidation
|
||||
|
||||
from .Regions import create_regions, create_events
|
||||
from .Items import ManualItem
|
||||
from .Rules import set_rules
|
||||
from .Options import manual_options_data
|
||||
from .Helpers import is_item_enabled, get_option_value, remove_specific_item, resolve_yaml_option, format_state_prog_items_key, convert_string_to_itemclassification, ProgItemsCat
|
||||
from .container import APManualFile
|
||||
|
||||
from BaseClasses import CollectionState, ItemClassification, Item, Location
|
||||
from Options import PerGameCommonOptions
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
from .hooks.World import \
|
||||
hook_get_filler_item_name, before_create_regions, after_create_regions, \
|
||||
before_create_items_all, before_create_items_place_items, before_create_items_starting, before_create_items_filler, after_create_items, \
|
||||
before_create_item, after_create_item, \
|
||||
before_set_rules, after_set_rules, \
|
||||
before_generate_basic, \
|
||||
before_fill_slot_data, after_fill_slot_data, before_write_spoiler, \
|
||||
before_extend_hint_information, after_extend_hint_information, \
|
||||
after_collect_item, after_remove_item, before_generate_early, hook_interpret_slot_data
|
||||
|
||||
class ManualWorld(World):
|
||||
__doc__ = world_description
|
||||
game: ClassVar[str] = game_name
|
||||
web = world_webworld
|
||||
|
||||
options_dataclass = manual_options_data
|
||||
data_version = 2
|
||||
required_client_version = (0, 3, 4)
|
||||
|
||||
# These properties are set from the imports of the same name above.
|
||||
item_table = item_table
|
||||
location_table = location_table # this is likely imported from Data instead of Locations because the Game Complete location should not be in here, but is used for lookups
|
||||
event_table = event_table
|
||||
category_table = category_table
|
||||
|
||||
item_id_to_name = item_id_to_name
|
||||
item_name_to_id = item_name_to_id
|
||||
item_name_to_item = item_name_to_item
|
||||
item_name_groups = item_name_groups
|
||||
|
||||
filler_item_name = filler_item_name
|
||||
|
||||
item_counts: dict[int, Counter[str]] = {}
|
||||
item_counts_progression: dict[int, Counter[str]] = {}
|
||||
start_inventory = {}
|
||||
|
||||
location_id_to_name = location_id_to_name
|
||||
location_name_to_id = location_name_to_id
|
||||
location_name_to_location = location_name_to_location
|
||||
location_name_groups = location_name_groups
|
||||
victory_names = victory_names
|
||||
|
||||
event_name_to_event = event_name_to_event
|
||||
event_name_groups = event_name_groups
|
||||
item_and_event_name_groups: dict[str, set[str]] = dict(item_name_groups)
|
||||
for name, category in event_name_groups.items():
|
||||
if name in item_and_event_name_groups.keys():
|
||||
item_and_event_name_groups[name] = item_name_groups[name].union(category)
|
||||
else:
|
||||
item_and_event_name_groups[name] = category
|
||||
|
||||
# UT (the universal-est of trackers) can now generate without a YAML
|
||||
ut_can_gen_without_yaml = True
|
||||
glitches_item_name: str | None = glitches_item_name
|
||||
|
||||
origin_region_name = "Manual"
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
hook_result = hook_get_filler_item_name(self, self.multiworld, self.player)
|
||||
if hook_result and isinstance(hook_result, str):
|
||||
return hook_result
|
||||
elif hook_result and isinstance(hook_result, list):
|
||||
return self.random.choice(hook_result)
|
||||
elif isinstance(filler_item_name, str):
|
||||
return self.filler_item_name
|
||||
else:
|
||||
random_filler_name = self.random.choice(filler_item_name)
|
||||
return random_filler_name
|
||||
|
||||
def interpret_slot_data(self, slot_data: dict[str, Any]) -> dict[str, Any]:
|
||||
#this is called by tools like UT
|
||||
if not slot_data:
|
||||
return {}
|
||||
|
||||
regen = hook_interpret_slot_data(self, self.player, slot_data) or slot_data
|
||||
return regen
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld) -> None:
|
||||
runGenerationDataValidation(cls)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
before_generate_early(self, self.multiworld, self.player)
|
||||
for item in item_table:
|
||||
if item.get("local"):
|
||||
if item.get("name") not in self.options.local_items.value:
|
||||
self.options.local_items.value.add(item["name"])
|
||||
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
slot_data = self.multiworld.re_gen_passthrough.get(self.game, {})
|
||||
if slot_data:
|
||||
for key, value in slot_data.items():
|
||||
if hasattr(self.options, key):
|
||||
getattr(self.options, key).value = value
|
||||
|
||||
def create_regions(self):
|
||||
before_create_regions(self, self.multiworld, self.player)
|
||||
|
||||
create_regions(self, self.multiworld, self.player)
|
||||
|
||||
create_events(self, self.multiworld, self.player)
|
||||
|
||||
location_game_complete = self.multiworld.get_location(victory_names[get_option_value(self.multiworld, self.player, 'goal')], self.player)
|
||||
location_game_complete.address = None
|
||||
|
||||
if not unused_goals_are_locations:
|
||||
for unused_goal in [self.multiworld.get_location(name, self.player) for name in victory_names if name != location_game_complete.name]:
|
||||
unused_goal.parent_region.locations.remove(unused_goal)
|
||||
|
||||
location_game_complete.place_locked_item(
|
||||
ManualItem("__Victory__", ItemClassification.progression, None, player=self.player))
|
||||
|
||||
after_create_regions(self, self.multiworld, self.player)
|
||||
|
||||
def create_items(self):
|
||||
# Generate item pool
|
||||
pool: list[Item] = []
|
||||
traps = []
|
||||
configured_item_names = self.item_id_to_name.copy()
|
||||
is_UT = bool(getattr(self.multiworld, "generation_is_fake", False))
|
||||
|
||||
items_config: dict[str, int|dict[ItemClassification | str | int, int]] = {}
|
||||
for name in configured_item_names.values():
|
||||
if name == "__Victory__": continue
|
||||
# intentionally using the Game.py filler_item_name here because it can be a non-Items item
|
||||
if isinstance(filler_item_name, str):
|
||||
if name == filler_item_name: continue
|
||||
elif isinstance(filler_item_name, list):
|
||||
if name in filler_item_name: continue
|
||||
|
||||
item = self.item_name_to_item[name]
|
||||
item_count = int(item.get("count", 1))
|
||||
|
||||
if item.get("trap"):
|
||||
traps.append(name)
|
||||
|
||||
if not is_item_enabled(self.multiworld, self.player, item):
|
||||
items_config[name] = 0
|
||||
|
||||
else:
|
||||
if item.get("classification_count"):
|
||||
items_config[name] = item["classification_count"]
|
||||
|
||||
else:
|
||||
items_config[name] = item_count
|
||||
|
||||
items_config = before_create_items_all(items_config, self, self.multiworld, self.player)
|
||||
|
||||
for name, configs in items_config.items():
|
||||
total_created = 0
|
||||
if type(configs) is int:
|
||||
total_created = configs
|
||||
for _ in range(configs):
|
||||
new_item = self.create_item(name)
|
||||
pool.append(new_item)
|
||||
elif type(configs) is dict:
|
||||
for override, count in configs.items():
|
||||
total_created += count
|
||||
if isinstance(override, ItemClassification):
|
||||
true_class = override
|
||||
else:
|
||||
try:
|
||||
if isinstance(override, int):
|
||||
true_class = ItemClassification(override)
|
||||
else:
|
||||
true_class = convert_string_to_itemclassification(override)
|
||||
except Exception as ex:
|
||||
raise Exception(f"Item override '{override}' for {name} improperly defined\n\n{type(ex).__name__}:{ex}")
|
||||
|
||||
for _ in range(count):
|
||||
new_item = self.create_item(name, true_class)
|
||||
pool.append(new_item)
|
||||
else:
|
||||
raise Exception(f"Item override for {name} improperly defined")
|
||||
|
||||
if total_created == 0 or is_UT: continue
|
||||
|
||||
item = self.item_name_to_item[name]
|
||||
if item.get("early"): # Some or all early
|
||||
if isinstance(item["early"],int) or (isinstance(item["early"],str) and item["early"].isnumeric()):
|
||||
self.multiworld.early_items[self.player][name] = int(item["early"])
|
||||
|
||||
elif isinstance(item["early"],bool): #No need to deal with true vs false since false wont get here
|
||||
self.multiworld.early_items[self.player][name] = total_created
|
||||
|
||||
else:
|
||||
raise Exception(f"Item {name}'s 'early' has an invalid value of '{item['early']}'. \nA boolean or an integer was expected.")
|
||||
|
||||
if item.get("local_early"): # Some or all local and early
|
||||
if isinstance(item["local_early"],int) or (isinstance(item["local_early"],str) and item["local_early"].isnumeric()):
|
||||
self.multiworld.local_early_items[self.player][name] = int(item["local_early"])
|
||||
|
||||
elif isinstance(item["local_early"],bool):
|
||||
self.multiworld.local_early_items[self.player][name] = total_created
|
||||
|
||||
else:
|
||||
raise Exception(f"Item {name}'s 'local_early' has an invalid value of '{item['local_early']}'. \nA boolean or an integer was expected.")
|
||||
|
||||
# Handle item forbidding/placement
|
||||
pool = before_create_items_place_items(pool, self, self.multiworld, self.player)
|
||||
locations_with_forbid: list[Location] = []
|
||||
locations_with_placements: list[Location] = []
|
||||
if not is_UT:
|
||||
for location in self.multiworld.get_unfilled_locations(player=self.player):
|
||||
manual_location = self.location_name_to_location.get(location.name, {})
|
||||
if manual_location.get("place_item") or manual_location.get("place_item_category"):
|
||||
locations_with_placements.append(location)
|
||||
elif manual_location.get("dont_place_item") or manual_location.get("dont_place_item_category"):
|
||||
locations_with_forbid.append(location)
|
||||
|
||||
# Handle specific item forbidding using forbid_items_for_player
|
||||
for location in locations_with_forbid:
|
||||
manual_location = self.location_name_to_location.get(location.name, {})
|
||||
forbidden_item_names: set[str] = set()
|
||||
|
||||
if manual_location.get("dont_place_item"):
|
||||
forbidden_item_names |= set(manual_location["dont_place_item"])
|
||||
|
||||
if manual_location.get("dont_place_item_category"):
|
||||
for cat in manual_location["dont_place_item_category"]:
|
||||
forbidden_item_names |= self.item_name_groups.get(cat, set())
|
||||
|
||||
if forbidden_item_names:
|
||||
forbid_items_for_player(location, set(forbidden_item_names), self.player)
|
||||
|
||||
# Handle specific item placements using place_locked_item
|
||||
for location in locations_with_placements:
|
||||
manual_location = self.location_name_to_location.get(location.name, {})
|
||||
eligible_items = []
|
||||
eligible_item_names: set[str] = set()
|
||||
forbidden_item_names = set()
|
||||
place_messages = []
|
||||
forbid_messages = []
|
||||
|
||||
#First we get possible items names
|
||||
if manual_location.get("place_item"):
|
||||
eligible_item_names |= set(manual_location["place_item"])
|
||||
place_messages.append('", "'.join(manual_location["place_item"]))
|
||||
|
||||
if manual_location.get("place_item_category"):
|
||||
for cat in manual_location["place_item_category"]:
|
||||
eligible_item_names |= self.item_name_groups.get(cat, set())
|
||||
place_messages.append('", "'.join(manual_location["place_item_category"]) + " category(ies)")
|
||||
|
||||
# Second we check for forbidden items names
|
||||
if manual_location.get("dont_place_item"):
|
||||
forbidden_item_names |= set(manual_location["dont_place_item"])
|
||||
forbid_messages.append('", "'.join(manual_location["dont_place_item"]) + ' items')
|
||||
|
||||
if manual_location.get("dont_place_item_category"):
|
||||
for cat in manual_location["dont_place_item_category"]:
|
||||
forbidden_item_names |= self.item_name_groups.get(cat, set())
|
||||
forbid_messages.append('", "'.join(manual_location["dont_place_item_category"]) + ' category(ies)')
|
||||
|
||||
# If we forbid some names, check for those in the possible names and remove them
|
||||
if forbidden_item_names:
|
||||
eligible_item_names = {name for name in eligible_item_names if name not in forbidden_item_names}
|
||||
|
||||
if eligible_item_names:
|
||||
eligible_items = [item for item in pool if item.name in eligible_item_names]
|
||||
|
||||
if len(eligible_items) == 0:
|
||||
nl = "\n"
|
||||
if forbidden_item_names:
|
||||
raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}".\n No items that match "{f"{nl} or ".join(place_messages)}"\n Maybe because of forbidden "{f"{nl} or ".join(forbid_messages)}"')
|
||||
raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}". \n No items that match "{f"{nl} or ".join(place_messages)}"')
|
||||
|
||||
item_to_place = self.random.choice(eligible_items)
|
||||
location.place_locked_item(item_to_place)
|
||||
|
||||
# remove the item we're about to place from the pool so it isn't placed twice
|
||||
remove_specific_item(pool, item_to_place)
|
||||
|
||||
# Handle game.json's starting_items
|
||||
pool = before_create_items_starting(pool, self, self.multiworld, self.player)
|
||||
|
||||
items_started: list[Item] = []
|
||||
|
||||
if starting_items:
|
||||
for starting_item_block in starting_items:
|
||||
if not resolve_yaml_option(self.multiworld, self.player, starting_item_block):
|
||||
continue
|
||||
# if there's a condition on having a previous item, check for any of them
|
||||
# if not found in items started, this starting item rule shouldn't execute, and check the next one
|
||||
if "if_previous_item" in starting_item_block:
|
||||
matching_items = [item for item in items_started if item.name in starting_item_block["if_previous_item"]]
|
||||
|
||||
if len(matching_items) == 0:
|
||||
continue
|
||||
|
||||
# start with the full pool of items
|
||||
items = pool
|
||||
|
||||
# if the setting lists specific item names, limit the items to just those
|
||||
if "items" in starting_item_block:
|
||||
items = [item for item in pool if item.name in starting_item_block["items"]]
|
||||
|
||||
# if the setting lists specific item categories, limit the items to ones that have any of those categories
|
||||
if "item_categories" in starting_item_block:
|
||||
items_in_categories = [item["name"] for item in self.item_name_to_item.values() if "category" in item and len(set(starting_item_block["item_categories"]).intersection(item["category"])) > 0]
|
||||
items = [item for item in pool if item.name in items_in_categories]
|
||||
|
||||
self.random.shuffle(items)
|
||||
|
||||
# if the setting lists a specific number of random items that should be pulled, only use a subset equal to that number
|
||||
if "random" in starting_item_block:
|
||||
items = items[0:starting_item_block["random"]]
|
||||
|
||||
for starting_item in items:
|
||||
items_started.append(starting_item)
|
||||
self.multiworld.push_precollected(starting_item)
|
||||
remove_specific_item(pool, starting_item)
|
||||
|
||||
self.start_inventory = {i.name: items_started.count(i) for i in items_started}
|
||||
|
||||
pool = before_create_items_filler(pool, self, self.multiworld, self.player)
|
||||
pool = self.adjust_filler_items(pool, traps)
|
||||
pool = after_create_items(pool, self, self.multiworld, self.player)
|
||||
|
||||
# need to put all of the items in the pool so we can have a full state for placement
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
# Preparing to count the items:
|
||||
precollected_items = list(self.multiworld.precollected_items[self.player])
|
||||
|
||||
# UT doesn't precollect the exceptions so this can be skipped
|
||||
if not is_UT:
|
||||
# Filter Precollected items for those not in logic aka created by start_inventory(_from_pool)
|
||||
precollected_exceptions = self.options.start_inventory.value + self.options.start_inventory_from_pool.value # type: ignore
|
||||
for item, count in precollected_exceptions.items():
|
||||
items_iter = iter([i for i in precollected_items if i.name == item])
|
||||
for _ in range(count):
|
||||
precollected_items.remove(next(items_iter))
|
||||
# Placed items:
|
||||
placed_pool: list[Item] = []
|
||||
for location in self.multiworld.get_filled_locations(self.player):
|
||||
if location.item is not None: # For type checkers
|
||||
placed_pool.append(location.item)
|
||||
|
||||
real_pool = pool + precollected_items + placed_pool
|
||||
self.item_counts[self.player] = self.get_item_counts(pool=real_pool)
|
||||
self.item_counts_progression[self.player] = self.get_item_counts(pool=real_pool, only_progression=True)
|
||||
|
||||
def create_item(self, name: str, class_override: Optional['ItemClassification']=None) -> Item:
|
||||
name = before_create_item(name, self, self.multiworld, self.player)
|
||||
|
||||
if name == self.glitches_item_name:
|
||||
return ManualItem(name, class_override or ItemClassification.progression, None, self.player)
|
||||
|
||||
item = self.item_name_to_item[name]
|
||||
classification: ItemClassification = ItemClassification.filler
|
||||
if class_override is not None:
|
||||
classification = class_override
|
||||
|
||||
elif item.get("classification_count"):
|
||||
# This should only be run if create_item is called outside of create_items
|
||||
not_prog_classes: list[ItemClassification] = []
|
||||
progression_classes: list[ItemClassification] = []
|
||||
for cat, count in item["classification_count"].items():
|
||||
if count:
|
||||
true_class = convert_string_to_itemclassification(cat)
|
||||
if ItemClassification.progression in true_class:
|
||||
progression_classes.append(true_class)
|
||||
else:
|
||||
not_prog_classes.append(true_class)
|
||||
if progression_classes:
|
||||
classification |= self.random.choice(progression_classes)
|
||||
elif not_prog_classes:
|
||||
classification |= not_prog_classes[0]
|
||||
|
||||
else:
|
||||
if item.get("trap"):
|
||||
classification |= ItemClassification.trap
|
||||
|
||||
if item.get("useful"):
|
||||
classification |= ItemClassification.useful
|
||||
|
||||
if item.get("progression_skip_balancing"):
|
||||
classification |= ItemClassification.progression_skip_balancing
|
||||
elif item.get("progression"):
|
||||
classification |= ItemClassification.progression
|
||||
|
||||
item_object = ManualItem(name, classification,
|
||||
self.item_name_to_id[name], player=self.player)
|
||||
|
||||
item_object = after_create_item(item_object, self, self.multiworld, self.player)
|
||||
|
||||
return item_object
|
||||
|
||||
# Item Value need a tweaked collect and remove:
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
manual_item = self.item_name_to_item.get(item.name, {})
|
||||
if change and manual_item.get("value"):
|
||||
for key, value in manual_item["value"].items():
|
||||
state.prog_items[item.player][format_state_prog_items_key(ProgItemsCat.VALUE, key)] += int(value)
|
||||
after_collect_item(self, state, change, item)
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().remove(state, item)
|
||||
manual_item = self.item_name_to_item.get(item.name, {})
|
||||
if change and manual_item.get("value"):
|
||||
for key, value in manual_item["value"].items():
|
||||
state.prog_items[item.player][format_state_prog_items_key(ProgItemsCat.VALUE, key)] -= int(value)
|
||||
after_remove_item(self, state, change, item)
|
||||
return change
|
||||
|
||||
def set_rules(self):
|
||||
before_set_rules(self, self.multiworld, self.player)
|
||||
|
||||
set_rules(self, self.multiworld, self.player)
|
||||
|
||||
after_set_rules(self, self.multiworld, self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
before_generate_basic(self, self.multiworld, self.player)
|
||||
|
||||
# Enable this in Meta.json to generate a diagram of your manual. Only works on 0.4.4+
|
||||
if get_option_value(self.multiworld, self.player, "generate_region_diagram"):
|
||||
from Utils import visualize_regions
|
||||
visualize_regions(self.multiworld.get_region("Manual", self.player), f"{self.game}_{self.player}.puml")
|
||||
|
||||
def pre_fill(self):
|
||||
# DataValidation after all the hooks are done but before fill
|
||||
runPreFillDataValidation(self, self.multiworld)
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = before_fill_slot_data({}, self, self.multiworld, self.player)
|
||||
|
||||
# slot_data["DeathLink"] = bool(self.multiworld.death_link[self.player].value)
|
||||
common_options = set(PerGameCommonOptions.type_hints.keys())
|
||||
common_options |= set(["generate_region_diagram", "start_inventory_from_pool"])
|
||||
for option_key, _ in self.options_dataclass.type_hints.items():
|
||||
if option_key in common_options:
|
||||
continue
|
||||
slot_data[option_key] = get_option_value(self.multiworld, self.player, option_key)
|
||||
|
||||
slot_data["visible_events"] = {}
|
||||
for _, event in self.event_name_to_event.items():
|
||||
event_name = event["name"]
|
||||
if event["visible"] and event_name not in slot_data["visible_events"]:
|
||||
slot_data["visible_events"][event_name] = event.get("category", [])
|
||||
elif event_name in slot_data["visible_events"]:
|
||||
temp_list = event.get("category", []) + slot_data["visible_events"][event_name]
|
||||
slot_data["visible_events"][event_name] = list(set(temp_list))
|
||||
|
||||
slot_data = after_fill_slot_data(slot_data, self, self.multiworld, self.player)
|
||||
|
||||
return slot_data
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmanual"
|
||||
zf_path = os.path.join(output_directory, filename)
|
||||
|
||||
apmanual = APManualFile(zf_path, player=self.player, player_name=self.player_name)
|
||||
apmanual.write()
|
||||
|
||||
if get_option_value(self.multiworld, self.player, "generate_region_diagram"):
|
||||
from Utils import visualize_regions
|
||||
visualize_regions(self.multiworld.get_region("Manual", self.player), f"{self.game}_{self.player}_spoiler.puml")
|
||||
|
||||
|
||||
def write_spoiler(self, spoiler_handle):
|
||||
before_write_spoiler(self, self.multiworld, spoiler_handle)
|
||||
|
||||
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
|
||||
before_extend_hint_information(hint_data, self, self.multiworld, self.player)
|
||||
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if not location.address:
|
||||
continue
|
||||
if "hint_entrance" in self.location_name_to_location[location.name]:
|
||||
if self.player not in hint_data:
|
||||
hint_data.update({self.player: {}})
|
||||
hint_data[self.player][location.address] = self.location_name_to_location[location.name]["hint_entrance"]
|
||||
|
||||
after_extend_hint_information(hint_data, self, self.multiworld, self.player)
|
||||
|
||||
###
|
||||
# Non-standard AP world methods
|
||||
###
|
||||
|
||||
rules_functions_maximum_recursion: int = 5
|
||||
"""Default: 5\n
|
||||
The maximum time a location/region's requirement can loop to check for functions\n
|
||||
One thing to remember is the more you loop the longer generation will take. So probably leave it as is unless you really needs it."""
|
||||
|
||||
def add_filler_items(self, item_pool, traps):
|
||||
Utils.deprecate("You're calling the deprecated add_filler_items() function. Use the adjust_filler_items() function instead.")
|
||||
return self.adjust_filler_items(item_pool, traps)
|
||||
|
||||
def adjust_filler_items(self, item_pool, traps):
|
||||
extras = len(self.multiworld.get_unfilled_locations(player=self.player)) - len(item_pool)
|
||||
|
||||
if extras > 0:
|
||||
trap_percent = get_option_value(self.multiworld, self.player, "filler_traps")
|
||||
if not traps:
|
||||
trap_percent = 0
|
||||
|
||||
trap_count = extras * trap_percent // 100
|
||||
filler_count = extras - trap_count
|
||||
|
||||
for _ in range(0, trap_count):
|
||||
extra_item = self.create_item(self.random.choice(traps))
|
||||
item_pool.append(extra_item)
|
||||
|
||||
for _ in range(0, filler_count):
|
||||
extra_item = self.create_item(self.get_filler_item_name())
|
||||
item_pool.append(extra_item)
|
||||
elif extras < 0:
|
||||
logging.warning(f"{self.game} has more items than locations. {abs(extras)} non-progression items will be removed at random.")
|
||||
# Filler is only assigned if the item doesn't have any other tags, so it only has to be covered by itself.
|
||||
# Skip Balancing is also not covered due to how it's only supported when paired with Progression.
|
||||
# As a result, these cover every possible combination can be removed.
|
||||
fillers = [item for item in item_pool if item.classification == ItemClassification.filler]
|
||||
traps = [item for item in item_pool if item.classification == ItemClassification.trap]
|
||||
useful = [item for item in item_pool if item.classification == ItemClassification.useful]
|
||||
# Useful + Trap is classified separately so that it can have a unique priority ranking.
|
||||
useful_traps = [item for item in item_pool if
|
||||
ItemClassification.progression not in item.classification
|
||||
and ItemClassification.useful in item.classification
|
||||
and ItemClassification.trap in item.classification]
|
||||
self.random.shuffle(fillers)
|
||||
self.random.shuffle(traps)
|
||||
self.random.shuffle(useful)
|
||||
self.random.shuffle(useful_traps)
|
||||
for _ in range(0, abs(extras)):
|
||||
popped = None
|
||||
if fillers:
|
||||
popped = fillers.pop()
|
||||
elif traps:
|
||||
popped = traps.pop()
|
||||
elif useful:
|
||||
popped = useful.pop()
|
||||
elif useful_traps:
|
||||
popped = useful_traps.pop()
|
||||
else:
|
||||
logging.warning("Could not remove enough non-progression items from the pool.")
|
||||
break
|
||||
remove_specific_item(item_pool, popped)
|
||||
|
||||
return item_pool
|
||||
|
||||
def get_item_counts(self, player: Optional[int] = None, pool: list[Item] | None | bool = None, only_progression: bool = False) -> Counter[str]:
|
||||
"""Returns the player real item counts.\n
|
||||
If you provide an item pool using the pool argument, then it's item counts will be returned.
|
||||
Otherwise, this function will only work after create_items, before then an empty Counter is returned.\n
|
||||
The only_progression argument let you filter the items to only get the count of progression items."""
|
||||
if player is None:
|
||||
player = self.player
|
||||
|
||||
if isinstance(pool, bool):
|
||||
Utils.deprecate("the 'reset' argument of get_item_counts has been deprecated to increase the stability of item counts.\
|
||||
\nIt should be removed. If you require a new up to date count you can get it using the 'pool' argument.\
|
||||
\nThat result wont be saved to world unless you override the values of world.item_counts_progression or world.item_counts depending on if you counted only the items with progresion or not.")
|
||||
pool = None
|
||||
|
||||
if pool is not None:
|
||||
return Counter([i.name for i in pool if not only_progression or i.advancement])
|
||||
|
||||
if only_progression:
|
||||
return self.item_counts_progression.get(player, Counter())
|
||||
else:
|
||||
return self.item_counts.get(player, Counter())
|
||||
|
||||
|
||||
###
|
||||
# Non-world client methods
|
||||
###
|
||||
|
||||
def launch_client(*args):
|
||||
import CommonClient
|
||||
from .ManualClient import launch as Main
|
||||
|
||||
if CommonClient.gui_enabled:
|
||||
launch(Main, name="Manual client", args=args)
|
||||
else:
|
||||
Main(*args)
|
||||
|
||||
class VersionedComponent(Component):
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, func: Optional[Callable] = None, version: int = 0, file_identifier: Optional[Callable[[str], bool]] = None, icon: Optional[str] = None):
|
||||
super().__init__(display_name=display_name, script_name=script_name, func=func, component_type=Type.CLIENT, file_identifier=file_identifier, icon=icon)
|
||||
self.version = version
|
||||
|
||||
def add_client_to_launcher() -> None:
|
||||
version = 2026_09_18 # YYYYMMDD
|
||||
found = False
|
||||
|
||||
if "manual" not in icon_paths:
|
||||
icon_paths["manual"] = Utils.user_path('data', 'manual.png')
|
||||
|
||||
discord_component = None
|
||||
for c in components:
|
||||
if c.display_name == "Manual Client":
|
||||
found = True
|
||||
if getattr(c, "version", 0) < version: # We have a newer version of the Manual Client than the one the last apworld added
|
||||
c.version = version
|
||||
c.func = launch_client
|
||||
c.icon = "manual"
|
||||
elif c.display_name == "Manual Discord Server":
|
||||
discord_component = c
|
||||
|
||||
if not found:
|
||||
components.append(VersionedComponent("Manual Client", "ManualClient", func=launch_client, version=version, file_identifier=SuffixIdentifier('.apmanual'), icon="manual"))
|
||||
if not discord_component:
|
||||
components.append(Component("Manual Discord Server", "ManualDiscord", func=lambda: webbrowser.open("https://discord.gg/hm4rQnTzQ5"), icon="discord", component_type=Type.ADJUSTER))
|
||||
|
||||
add_client_to_launcher()
|
||||
Reference in new issue
Block a user