Skip to content

Commit

Permalink
Merge pull request Ziktofel#294 from Salzkorn/sc2-next
Browse files Browse the repository at this point in the history
Add Item entry rule to custom mission orders
  • Loading branch information
Ziktofel authored Sep 2, 2024
2 parents 11f1ebb + 6811017 commit 3195a2f
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 98 deletions.
34 changes: 30 additions & 4 deletions worlds/sc2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from . import item_names
from .items import (
StarcraftItem, filler_items, get_full_item_list, ProtossItemType,
get_basic_units, ItemData, upgrade_included_names, kerrigan_actives, kerrigan_passives,
ItemData, kerrigan_actives, kerrigan_passives,
not_balanced_starting_units,
)
from . import items
Expand All @@ -23,7 +23,9 @@
KerriganPresence, KerriganPrimalStatus, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence,
get_enabled_campaigns, SpearOfAdunAutonomouslyCastAbilityPresence, Starcraft2Options,
GrantStoryTech, GenericUpgradeResearch, GenericUpgradeItems, RequiredTactics,
upgrade_included_names
)
from .rules import get_basic_units
from . import settings
from .pool_filter import filter_items
from .mission_tables import (
Expand All @@ -45,8 +47,12 @@ class ItemFilterFlags(enum.IntFlag):
Excluded = enum.auto()
AllowedOrphan = enum.auto()
"""Used to flag items that shouldn't be filtered out with their parents"""
ForceProgression = enum.auto()
"""Used to flag items that aren't classified as progression by default"""
Necessary = enum.auto()
"""Used to flag items that are never allowed to be culled"""

Unremovable = Locked|StartInventory|Plando
Unremovable = Locked|StartInventory|Plando|Necessary


@dataclass
Expand Down Expand Up @@ -145,6 +151,7 @@ def create_items(self):
flag_user_excluded_item_sets(self, item_list)
flag_war_council_items(self, item_list)
flag_and_add_resource_locations(self, item_list)
flag_mission_order_required_items(self, item_list)
pool: List[Item] = prune_item_pool(self, item_list)
pad_item_pool_with_filler(self, len(self.location_cache) - len(self.locked_locations) - len(pool), pool)

Expand All @@ -156,7 +163,10 @@ def set_rules(self) -> None:
if self.options.required_tactics == RequiredTactics.option_no_logic:
# Forcing completed goal and minimal accessibility on no logic
self.options.accessibility.value = Accessibility.option_minimal
self.multiworld.completion_condition[self.player] = lambda state: True
required_items = self.custom_mission_order.get_items_to_lock()
self.multiworld.completion_condition[self.player] = lambda state, required_items=required_items: all(
state.has(item, self.player, amount) for (item, amount) in required_items.items()
)
else:
self.multiworld.completion_condition[self.player] = self.custom_mission_order.get_completion_condition(self.player)

Expand Down Expand Up @@ -631,6 +641,17 @@ def flag_and_add_resource_locations(world: SC2World, item_list: List[FilterItem]
world.locked_locations.append(location.name)


def flag_mission_order_required_items(world: SC2World, item_list: List[FilterItem]) -> None:
"""Marks items that are necessary for item rules in the mission order and forces them to be progression."""
locks_required = world.custom_mission_order.get_items_to_lock()
locks_done = {item: 0 for item in locks_required}
for item in item_list:
if item.name in locks_required and locks_done[item.name] < locks_required[item.name]:
item.flags |= ItemFilterFlags.Necessary
item.flags |= ItemFilterFlags.ForceProgression
locks_done[item.name] += 1


def prune_item_pool(world: SC2World, item_list: List[FilterItem]) -> List[Item]:
"""Prunes the item pool size to be less than the number of available locations"""

Expand All @@ -648,17 +669,22 @@ def prune_item_pool(world: SC2World, item_list: List[FilterItem]) -> List[Item]:
pool: List[Item] = []
locked_items: List[Item] = []
existing_items: List[Item] = []
necessary_items: List[Item] = []
for item in item_list:
ap_item = create_item_with_correct_settings(world.player, item.name)
if ItemFilterFlags.ForceProgression in item.flags:
ap_item.classification = ItemClassification.progression
if ItemFilterFlags.StartInventory in item.flags:
existing_items.append(ap_item)
elif ItemFilterFlags.Necessary in item.flags:
necessary_items.append(ap_item)
elif ItemFilterFlags.Locked in item.flags:
locked_items.append(ap_item)
else:
pool.append(ap_item)

fill_pool_with_kerrigan_levels(world, pool)
filtered_pool = filter_items(world, world.location_cache, pool, existing_items, locked_items)
filtered_pool = filter_items(world, world.location_cache, pool, existing_items, locked_items, necessary_items)
return filtered_pool


Expand Down
14 changes: 9 additions & 5 deletions worlds/sc2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
from worlds._sc2common.bot.player import Bot
from .items import (
lookup_id_to_name, get_full_item_list, ItemData,
race_to_item_type, ZergItemType, ProtossItemType, upgrade_bundles, upgrade_included_names,
race_to_item_type, ZergItemType, ProtossItemType, upgrade_bundles,
WEAPON_ARMOR_UPGRADE_MAX_LEVEL,
)
from .locations import SC2WOL_LOC_ID_OFFSET, LocationType, LocationFlag, SC2HOTS_LOC_ID_OFFSET
Expand All @@ -64,7 +64,7 @@
)

import colorama
from .options import Option
from .options import Option, upgrade_included_names
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
from MultiServer import mark_raw

Expand Down Expand Up @@ -1389,20 +1389,24 @@ def calc_available_nodes(ctx: SC2Context) -> typing.Tuple[typing.List[int], typi
available_campaigns: typing.List[int] = []

beaten_missions = {mission_id for mission_id in ctx.mission_id_to_entry_rules if ctx.is_mission_completed(mission_id)}
received_items: typing.Dict[int, int] = {}
for network_item in ctx.items_received:
received_items.setdefault(network_item.item, 0)
received_items[network_item.item] += 1

accessible_rules: typing.Set[int] = set()
for campaign_idx, campaign in enumerate(ctx.custom_mission_order):
available_layouts[campaign_idx] = []
if campaign.entry_rule.is_accessible(beaten_missions, ctx.mission_id_to_entry_rules, accessible_rules, set()):
if campaign.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()):
available_campaigns.append(campaign_idx)
for layout_idx, layout in enumerate(campaign.layouts):
if layout.entry_rule.is_accessible(beaten_missions, ctx.mission_id_to_entry_rules, accessible_rules, set()):
if layout.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()):
available_layouts[campaign_idx].append(layout_idx)
for column in layout.missions:
for mission in column:
if mission.mission_id == -1:
continue
if mission.entry_rule.is_accessible(beaten_missions, ctx.mission_id_to_entry_rules, accessible_rules, set()):
if mission.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()):
available_missions.append(mission.mission_id)

return available_missions, available_layouts, available_campaigns
Expand Down
12 changes: 11 additions & 1 deletion worlds/sc2/docs/en_Custom Mission Orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,18 +262,22 @@ entry_rules: []
```
This defines access restrictions for parts of the mission order.

There are three available rules:
These are the available rules:
```yaml
entry_rules:
# Beat these things ("Beat rule")
- scope: []
# Beat X amount of missions from these things ("Count rule")
- scope: []
amount: -1
# Find these items ("Item rule")
- items: {}
# Fulfill X amount of other conditions ("Subrule rule")
- rules: []
amount: -1
```
Note that Item rules take both a name and amount for each item (see the example below). In general this rule treats items like the `locked_items` option, but as a notable difference all items required for Item rules are marked as progression. If multiple Item rules require the same item, the largest required amount will be locked, **not** the sum of all amounts.

The Beat and Count rules both require a list of scopes. This list accepts addresses towards other parts of the mission order.

The basic form of an address is `<Campaign>/<Layout>/<Mission>`, where `<Campaign>` and `<Layout>` are the definition names (not `display_names`!) of a campaign and a layout within that campaign, and `<Mission>` is the index of a mission slot in that layout. The indices of mission slots are determined by the layout's type.
Expand All @@ -296,6 +300,12 @@ Below are examples of the available entry rules:
Some Missions:
type: grid
size: 9
entry_rules:
# Item rule:
# To access the Some Missions layout,
# you have to find or receive your Marine
- items:
Marine: 1
Wings of Liberty:
Mar Sara:
Expand Down
55 changes: 0 additions & 55 deletions worlds/sc2/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import typing
import enum

from .options import get_option_value, RequiredTactics, GenericUpgradeItems
from .mission_tables import SC2Mission, SC2Race, SC2Campaign, campaign_mission_table
from . import item_names
from worlds.AutoWorld import World
Expand Down Expand Up @@ -1964,16 +1963,6 @@ def get_item_table():
}


def get_basic_units(world: 'SC2World', race: SC2Race) -> typing.Set[str]:
logic_level = get_option_value(world, 'required_tactics')
if logic_level == RequiredTactics.option_no_logic:
return no_logic_basic_units[race]
elif logic_level == RequiredTactics.option_advanced:
return advanced_basic_units[race]
else:
return basic_units[race]


# Items that can be placed before resources if not already in
# General upgrades and Mercs
second_pass_placeable_items: typing.Tuple[str, ...] = (
Expand Down Expand Up @@ -2289,50 +2278,6 @@ def get_basic_units(world: 'SC2World', race: SC2Race) -> typing.Set[str]:
],
}

# Names of upgrades to be included for different options
upgrade_included_names: Dict[GenericUpgradeItems, Set[str]] = {
GenericUpgradeItems.option_individual_items: {
item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON,
item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR,
item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON,
item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR,
item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON,
item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR,
item_names.PROGRESSIVE_ZERG_MELEE_ATTACK,
item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK,
item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE,
item_names.PROGRESSIVE_ZERG_FLYER_ATTACK,
item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE,
item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON,
item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR,
item_names.PROGRESSIVE_PROTOSS_SHIELDS,
item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON,
item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR,
},
GenericUpgradeItems.option_bundle_weapon_and_armor: {
item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE,
item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE,
item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE,
item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE,
},
GenericUpgradeItems.option_bundle_unit_class: {
item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE,
item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE,
item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE,
item_names.PROGRESSIVE_ZERG_GROUND_UPGRADE,
item_names.PROGRESSIVE_ZERG_FLYER_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE,
},
GenericUpgradeItems.option_bundle_all: {
item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE,
item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE,
}
}

lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
data.code}

Expand Down
80 changes: 73 additions & 7 deletions worlds/sc2/mission_order/entry_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dataclasses import dataclass

from ..mission_tables import SC2Mission
from ..items import item_table
from BaseClasses import CollectionState

if TYPE_CHECKING:
Expand Down Expand Up @@ -67,7 +68,8 @@ def shows_single_rule(self) -> bool:

@abstractmethod
def is_accessible(
self, beaten_missions: Set[int], mission_id_to_entry_rules: Dict[int, MissionEntryRules],
self, beaten_missions: Set[int], received_items: Dict[int, int],
mission_id_to_entry_rules: Dict[int, MissionEntryRules],
accessible_rules: Set[int], seen_rules: Set[int]
) -> bool:
return False
Expand Down Expand Up @@ -117,15 +119,16 @@ def shows_single_rule(self) -> bool:
return len(self.visual_reqs) == 1

def is_accessible(
self, beaten_missions: Set[int], mission_id_to_entry_rules: Dict[int, MissionEntryRules],
self, beaten_missions: Set[int], received_items: Dict[int, int],
mission_id_to_entry_rules: Dict[int, MissionEntryRules],
accessible_rules: Set[int], seen_rules: Set[int]
) -> bool:
# Beat rules are accessible if all their missions are beaten and accessible
if not beaten_missions.issuperset(self.mission_ids):
return False
for mission_id in self.mission_ids:
for rule in mission_id_to_entry_rules[mission_id]:
if not rule.is_accessible(beaten_missions, mission_id_to_entry_rules, accessible_rules, seen_rules):
if not rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules):
return False
return True

Expand Down Expand Up @@ -193,13 +196,14 @@ def shows_single_rule(self) -> bool:
return len(self.visual_reqs) == 1

def is_accessible(
self, beaten_missions: Set[int], mission_id_to_entry_rules: Dict[int, MissionEntryRules],
self, beaten_missions: Set[int], received_items: Dict[int, int],
mission_id_to_entry_rules: Dict[int, MissionEntryRules],
accessible_rules: Set[int], seen_rules: Set[int]
) -> bool:
# Count rules are accessible if enough of their missions are beaten and accessible
return self.amount <= sum(
all(
rule.is_accessible(beaten_missions, mission_id_to_entry_rules, accessible_rules, seen_rules)
rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules)
for rule in mission_id_to_entry_rules[mission_id]
)
for mission_id in beaten_missions.intersection(self.mission_ids)
Expand Down Expand Up @@ -259,6 +263,13 @@ def parse_from_dict(data: Dict[str, Any]) -> SubRuleRuleData:
for rule_data in data["sub_rules"]:
if "sub_rules" in rule_data:
rule = SubRuleRuleData.parse_from_dict(rule_data)
elif "item_ids" in rule_data:
# Slot data converts Dict[int, int] to Dict[str, int] for some reason
item_ids = {int(item): item_amount for (item, item_amount) in rule_data["item_ids"].items()}
rule = ItemRuleData(
item_ids,
rule_data["visual_reqs"]
)
elif "amount" in rule_data:
rule = CountMissionsRuleData(
**{field: value for field, value in rule_data.items()}
Expand Down Expand Up @@ -296,7 +307,8 @@ def shows_single_rule(self) -> bool:
return self.amount == len(self.sub_rules) == 1 and self.sub_rules[0].shows_single_rule()

def is_accessible(
self, beaten_missions: Set[int], mission_id_to_entry_rules: Dict[int, MissionEntryRules],
self, beaten_missions: Set[int], received_items: Dict[int, int],
mission_id_to_entry_rules: Dict[int, MissionEntryRules],
accessible_rules: Set[int], seen_rules: Set[int]
) -> bool:
# Early exit check for top-level entry rules
Expand All @@ -310,10 +322,64 @@ def is_accessible(
seen_rules.add(self.rule_id)
# Sub-rule rules are accessible if enough of their child rules are accessible
if self.amount <= sum(
rule.is_accessible(beaten_missions, mission_id_to_entry_rules, accessible_rules, seen_rules)
rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules)
for rule in self.sub_rules
):
if self.rule_id >= 0:
accessible_rules.add(self.rule_id)
return True
return False

class ItemEntryRule(EntryRule):
items_to_check: Dict[str, int]

def __init__(self, items_to_check: Dict[str, int]) -> None:
super().__init__()
self.items_to_check = items_to_check

def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission]) -> bool:
# Region creation should assume items can be placed
return True

def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
# Depth 0 means this rule requires 0 prior beaten missions
return 0

def to_lambda(self, player: int) -> Callable[[CollectionState], bool]:
return lambda state: state.has_all_counts(self.items_to_check, player)

def to_slot_data(self) -> RuleData:
item_ids = {item_table[item].code: amount for (item, amount) in self.items_to_check.items()}
visual_reqs = [item if amount == 1 else str(amount) + "x " + item for (item, amount) in self.items_to_check.items()]
return ItemRuleData(
item_ids,
visual_reqs
)

@dataclass
class ItemRuleData(RuleData):
item_ids: Dict[int, int]
visual_reqs: List[str]

def tooltip(self, indents: int, missions: Dict[int, SC2Mission]) -> str:
indent = " ".join("" for _ in range(indents))
if len(self.visual_reqs) == 1:
return f"Find {self.visual_reqs[0]}"
tooltip = f"Find all of these:\n{indent}- "
tooltip += f"\n{indent}- ".join(req for req in self.visual_reqs)
return tooltip

def shows_single_rule(self) -> bool:
return len(self.visual_reqs) == 1

def is_accessible(
self, beaten_missions: Set[int], received_items: Dict[int, int],
mission_id_to_entry_rules: Dict[int, MissionEntryRules],
accessible_rules: Set[int], seen_rules: Set[int]
) -> bool:
return all(
item in received_items and received_items[item] >= amount
for (item, amount) in self.item_ids.items()
)


Loading

0 comments on commit 3195a2f

Please sign in to comment.