mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00

* MC: add death_link option * Minecraft: 1.17 advancements and logic support * Update Minecraft tracker to 1.17 * Minecraft: add tests for new advancements * removed jdk/forge download install out of iss and into MinecraftClient.py using flag --install * Add required_bosses option choices are none, ender_dragon, wither, both postgame advancements are set according to the required boss for completion * fix docstring for PostgameAdvancements * Minecraft: add starting_items List of dicts: item, amount, nbt * Update descriptions for AdvancementGoal and EggShardsRequired * Minecraft: fix tests for required_bosses attribute * Minecraft: updated logic for various dragon-related advancements Split the logic into can_respawn and can_kill dragon Free the End, Monsters Hunted, The End Again still require both respawn and kill, since the player needs to kill and be credited with the kill You Need a Mint and Is It a Plane now require only respawn, since the dragon need only be alive; if killed out of logic, it's ok The Next Generation only requires kill, since the egg spawns regardless of whether the player was credited with the kill or not * Minecraft client: ignore prereleases unless --prerelease flag is on * explicitly state all defaults change structure shuffle and structure compass defaults to true update install tutorial to point to player-settings page, as well as removing instructions for manual install * Minecraft client: add Minecraft version check Adds a minecraft_version field in the apmc, and downloads only mods which contain that version in the name of the .jar file. This ensures that the client remains compatible even if new mods are released for later versions, since they won't download a mod for a later version than the apmc says. Co-authored-by: Kono Tyran <Kono.Tyran@gmail.com>
432 lines
13 KiB
Python
432 lines
13 KiB
Python
from __future__ import annotations
|
|
import typing
|
|
import random
|
|
|
|
|
|
class AssembleOptions(type):
|
|
def __new__(mcs, name, bases, attrs):
|
|
options = attrs["options"] = {}
|
|
name_lookup = attrs["name_lookup"] = {}
|
|
# merge parent class options
|
|
for base in bases:
|
|
if getattr(base, "options", None):
|
|
options.update(base.options)
|
|
name_lookup.update(base.name_lookup)
|
|
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
|
name.startswith("option_")}
|
|
if "random" in new_options:
|
|
raise Exception("Choice option 'random' cannot be manually assigned.")
|
|
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
|
options.update(new_options)
|
|
|
|
# apply aliases, without name_lookup
|
|
options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if
|
|
name.startswith("alias_")})
|
|
|
|
# auto-validate schema on __init__
|
|
if "schema" in attrs.keys():
|
|
def validate_decorator(func):
|
|
def validate(self, *args, **kwargs):
|
|
func(self, *args, **kwargs)
|
|
self.value = self.schema.validate(self.value)
|
|
|
|
return validate
|
|
|
|
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
|
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
|
|
|
|
|
class Option(metaclass=AssembleOptions):
|
|
value: int
|
|
name_lookup: typing.Dict[int, str]
|
|
default = 0
|
|
|
|
# convert option_name_long into Name Long as displayname, otherwise name_long is the result.
|
|
# Handled in get_option_name()
|
|
autodisplayname = False
|
|
|
|
# can be weighted between selections
|
|
supports_weighting = True
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
|
|
|
def __hash__(self):
|
|
return hash(self.value)
|
|
|
|
@property
|
|
def current_key(self) -> str:
|
|
return self.name_lookup[self.value]
|
|
|
|
def get_current_option_name(self) -> str:
|
|
"""For display purposes."""
|
|
return self.get_option_name(self.value)
|
|
|
|
@classmethod
|
|
def get_option_name(cls, value: typing.Any) -> str:
|
|
if cls.autodisplayname:
|
|
return cls.name_lookup[value].replace("_", " ").title()
|
|
else:
|
|
return cls.name_lookup[value]
|
|
|
|
def __int__(self) -> int:
|
|
return self.value
|
|
|
|
def __bool__(self) -> bool:
|
|
return bool(self.value)
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any):
|
|
raise NotImplementedError
|
|
|
|
|
|
class Toggle(Option):
|
|
option_false = 0
|
|
option_true = 1
|
|
default = 0
|
|
|
|
def __init__(self, value: int):
|
|
assert value == 0 or value == 1
|
|
self.value = value
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str) -> Toggle:
|
|
if text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
|
return cls(0)
|
|
else:
|
|
return cls(1)
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any):
|
|
if type(data) == str:
|
|
return cls.from_text(data)
|
|
else:
|
|
return cls(data)
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, Toggle):
|
|
return self.value == other.value
|
|
else:
|
|
return self.value == other
|
|
|
|
def __gt__(self, other):
|
|
if isinstance(other, Toggle):
|
|
return self.value > other.value
|
|
else:
|
|
return self.value > other
|
|
|
|
def __bool__(self):
|
|
return bool(self.value)
|
|
|
|
def __int__(self):
|
|
return int(self.value)
|
|
|
|
@classmethod
|
|
def get_option_name(cls, value):
|
|
return ["No", "Yes"][int(value)]
|
|
|
|
|
|
class DefaultOnToggle(Toggle):
|
|
default = 1
|
|
|
|
|
|
class Choice(Option):
|
|
autodisplayname = True
|
|
|
|
def __init__(self, value: int):
|
|
self.value: int = value
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str) -> Choice:
|
|
text = text.lower()
|
|
if text == "random":
|
|
return cls(random.choice(list(cls.name_lookup)))
|
|
for optionname, value in cls.options.items():
|
|
if optionname == text:
|
|
return cls(value)
|
|
raise KeyError(
|
|
f'Could not find option "{text}" for "{cls.__name__}", '
|
|
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any) -> Choice:
|
|
if type(data) == int and data in cls.options.values():
|
|
return cls(data)
|
|
return cls.from_text(str(data))
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
return other.value == self.value
|
|
elif isinstance(other, str):
|
|
assert other in self.options
|
|
return other == self.current_key
|
|
elif isinstance(other, int):
|
|
assert other in self.name_lookup
|
|
return other == self.value
|
|
elif isinstance(other, bool):
|
|
return other == bool(self.value)
|
|
else:
|
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
|
|
|
def __ne__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
return other.value != self.value
|
|
elif isinstance(other, str):
|
|
assert other in self.options
|
|
return other != self.current_key
|
|
elif isinstance(other, int):
|
|
assert other in self.name_lookup
|
|
return other != self.value
|
|
elif isinstance(other, bool):
|
|
return other != bool(self.value)
|
|
elif other is None:
|
|
return False
|
|
else:
|
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
|
|
|
|
|
class Range(Option, int):
|
|
range_start = 0
|
|
range_end = 1
|
|
|
|
def __init__(self, value: int):
|
|
if value < self.range_start:
|
|
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
|
elif value > self.range_end:
|
|
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
|
|
self.value = value
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str) -> Range:
|
|
text = text.lower()
|
|
if text.startswith("random"):
|
|
if text == "random-low":
|
|
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_start), 0)))
|
|
elif text == "random-high":
|
|
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
|
|
elif text == "random-middle":
|
|
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
|
|
else:
|
|
return cls(random.randint(cls.range_start, cls.range_end))
|
|
return cls(int(text))
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any) -> Range:
|
|
if type(data) == int:
|
|
return cls(data)
|
|
return cls.from_text(str(data))
|
|
|
|
def get_option_name(self, value):
|
|
return str(value)
|
|
|
|
def __str__(self):
|
|
return str(self.value)
|
|
|
|
|
|
class OptionNameSet(Option):
|
|
default = frozenset()
|
|
|
|
def __init__(self, value: typing.Set[str]):
|
|
self.value: typing.Set[str] = value
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str) -> OptionNameSet:
|
|
return cls({option.strip() for option in text.split(",")})
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any) -> OptionNameSet:
|
|
if type(data) == set:
|
|
return cls(data)
|
|
return cls.from_text(str(data))
|
|
|
|
|
|
class OptionDict(Option):
|
|
default = {}
|
|
supports_weighting = False
|
|
value: typing.Dict[str, typing.Any]
|
|
|
|
def __init__(self, value: typing.Dict[str, typing.Any]):
|
|
self.value = value
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
|
if type(data) == dict:
|
|
return cls(data)
|
|
else:
|
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
|
|
|
def get_option_name(self, value):
|
|
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
|
|
|
def __contains__(self, item):
|
|
return item in self.value
|
|
|
|
|
|
class ItemDict(OptionDict):
|
|
# implemented by Generate
|
|
verify_item_name = True
|
|
|
|
def __init__(self, value: typing.Dict[str, int]):
|
|
if any(item_count < 1 for item_count in value.values()):
|
|
raise Exception("Cannot have non-positive item counts.")
|
|
super(ItemDict, self).__init__(value)
|
|
|
|
|
|
class OptionList(Option):
|
|
default = []
|
|
supports_weighting = False
|
|
value: list
|
|
|
|
def __init__(self, value: typing.List[typing.Any]):
|
|
self.value = value or []
|
|
super(OptionList, self).__init__()
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str):
|
|
return cls([option.strip() for option in text.split(",")])
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any):
|
|
if type(data) == list:
|
|
return cls(data)
|
|
return cls.from_text(str(data))
|
|
|
|
def get_option_name(self, value):
|
|
return ", ".join(value)
|
|
|
|
def __contains__(self, item):
|
|
return item in self.value
|
|
|
|
|
|
class OptionSet(Option):
|
|
default = frozenset()
|
|
supports_weighting = False
|
|
value: set
|
|
|
|
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
|
|
self.value = set(value)
|
|
super(OptionSet, self).__init__()
|
|
|
|
@classmethod
|
|
def from_text(cls, text: str):
|
|
return cls([option.strip() for option in text.split(",")])
|
|
|
|
@classmethod
|
|
def from_any(cls, data: typing.Any):
|
|
if type(data) == list:
|
|
return cls(data)
|
|
elif type(data) == set:
|
|
return cls(data)
|
|
return cls.from_text(str(data))
|
|
|
|
def get_option_name(self, value):
|
|
return ", ".join(value)
|
|
|
|
def __contains__(self, item):
|
|
return item in self.value
|
|
|
|
|
|
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
|
|
|
|
|
class Accessibility(Choice):
|
|
"""Set rules for reachability of your items/locations.
|
|
Locations: ensure everything can be reached and acquired.
|
|
Items: ensure all logically relevant items can be acquired.
|
|
Minimal: ensure what is needed to reach your goal can be acquired."""
|
|
|
|
option_locations = 0
|
|
option_items = 1
|
|
option_minimal = 2
|
|
alias_none = 2
|
|
default = 1
|
|
|
|
|
|
class ProgressionBalancing(DefaultOnToggle):
|
|
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
|
|
|
|
|
|
common_options = {
|
|
"progression_balancing": ProgressionBalancing,
|
|
"accessibility": Accessibility
|
|
}
|
|
|
|
|
|
class ItemSet(OptionSet):
|
|
# implemented by Generate
|
|
verify_item_name = True
|
|
|
|
|
|
class LocalItems(ItemSet):
|
|
"""Forces these items to be in their native world."""
|
|
displayname = "Local Items"
|
|
|
|
|
|
class NonLocalItems(ItemSet):
|
|
"""Forces these items to be outside their native world."""
|
|
displayname = "Not Local Items"
|
|
|
|
|
|
class StartInventory(ItemDict):
|
|
"""Start with these items."""
|
|
verify_item_name = True
|
|
displayname = "Start Inventory"
|
|
|
|
|
|
class StartHints(ItemSet):
|
|
"""Start with these item's locations prefilled into the !hint command."""
|
|
displayname = "Start Hints"
|
|
|
|
|
|
class StartLocationHints(OptionSet):
|
|
"""Start with these locations and their item prefilled into the !hint command"""
|
|
displayname = "Start Location Hints"
|
|
|
|
|
|
class ExcludeLocations(OptionSet):
|
|
"""Prevent these locations from having an important item"""
|
|
displayname = "Excluded Locations"
|
|
verify_location_name = True
|
|
|
|
|
|
class DeathLink(Toggle):
|
|
"""When you die, everyone dies. Of course the reverse is true too."""
|
|
displayname = "Death Link"
|
|
|
|
|
|
per_game_common_options = {
|
|
"local_items": LocalItems,
|
|
"non_local_items": NonLocalItems,
|
|
"start_inventory": StartInventory,
|
|
"start_hints": StartHints,
|
|
"start_location_hints": StartLocationHints,
|
|
"exclude_locations": ExcludeLocations
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
|
|
from worlds.alttp.Options import Logic
|
|
import argparse
|
|
|
|
map_shuffle = Toggle
|
|
compass_shuffle = Toggle
|
|
keyshuffle = Toggle
|
|
bigkey_shuffle = Toggle
|
|
hints = Toggle
|
|
test = argparse.Namespace()
|
|
test.logic = Logic.from_text("no_logic")
|
|
test.map_shuffle = map_shuffle.from_text("ON")
|
|
test.hints = hints.from_text('OFF')
|
|
try:
|
|
test.logic = Logic.from_text("overworld_glitches_typo")
|
|
except KeyError as e:
|
|
print(e)
|
|
try:
|
|
test.logic_owg = Logic.from_text("owg")
|
|
except KeyError as e:
|
|
print(e)
|
|
if test.map_shuffle:
|
|
print("map_shuffle is on")
|
|
print(f"Hints are {bool(test.hints)}")
|
|
print(test)
|