OSRS: Fix UT Integration and Various Gen Failures (#5331)

This commit is contained in:
Faris
2025-08-16 16:08:44 -05:00
committed by GitHub
parent 9d654b7e3b
commit eb09be3594
6 changed files with 69 additions and 35 deletions

View File

@@ -3,6 +3,8 @@ import typing
from BaseClasses import Location from BaseClasses import Location
task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
class SkillRequirement(typing.NamedTuple): class SkillRequirement(typing.NamedTuple):
skill: str skill: str
level: int level: int

View File

@@ -8,7 +8,7 @@ import requests
# The CSVs are updated at this repository to be shared between generator and client. # The CSVs are updated at this repository to be shared between generator and client.
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/" data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
# The Github tag of the CSVs this was generated with # The Github tag of the CSVs this was generated with
data_csv_tag = "v2.0.4" data_csv_tag = "v2.0.5"
# If true, generate using file names in the repository # If true, generate using file names in the repository
debug = False debug = False

View File

@@ -77,7 +77,7 @@ location_rows = [
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2), LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [], [], 0), LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [SkillRequirement('Cooking', 32), ], [], 0),
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
LocationRow('Burn a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0), LocationRow('Burn a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0),

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from Options import Choice, Toggle, Range, PerGameCommonOptions from Options import Choice, Toggle, Range, PerGameCommonOptions
MAX_COMBAT_TASKS = 16 MAX_COMBAT_TASKS = 17
MAX_PRAYER_TASKS = 5 MAX_PRAYER_TASKS = 5
MAX_MAGIC_TASKS = 7 MAX_MAGIC_TASKS = 7

View File

@@ -190,6 +190,8 @@ def get_firemaking_skill_rule(level, player, options) -> CollectionRule:
def get_skill_rule(skill, level, player, options) -> CollectionRule: def get_skill_rule(skill, level, player, options) -> CollectionRule:
if level <= 1:
return lambda state: True
if skill.lower() == "fishing": if skill.lower() == "fishing":
return get_fishing_skill_rule(level, player, options) return get_fishing_skill_rule(level, player, options)
if skill.lower() == "mining": if skill.lower() == "mining":

View File

@@ -1,11 +1,11 @@
import typing import typing
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld
from Fill import fill_restrictive, FillError
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from Options import OptionError
from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
chunksanity_special_region_names chunksanity_special_region_names
from .Locations import OSRSLocation, LocationRow from .Locations import OSRSLocation, LocationRow, task_types
from .Rules import * from .Rules import *
from .Options import OSRSOptions, StartingArea from .Options import OSRSOptions, StartingArea
from .Names import LocationNames, ItemNames, RegionNames from .Names import LocationNames, ItemNames, RegionNames
@@ -47,6 +47,7 @@ class OSRSWorld(World):
base_id = 0x070000 base_id = 0x070000
data_version = 1 data_version = 1
explicit_indirect_conditions = False explicit_indirect_conditions = False
ut_can_gen_without_yaml = True
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))}
location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))}
@@ -105,6 +106,18 @@ class OSRSWorld(World):
# Set Starting Chunk # Set Starting Chunk
self.multiworld.push_precollected(self.create_item(self.starting_area_item)) self.multiworld.push_precollected(self.create_item(self.starting_area_item))
elif hasattr(self.multiworld,"re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough:
re_gen_passthrough = self.multiworld.re_gen_passthrough[self.game] # UT passthrough
if "starting_area" in re_gen_passthrough:
self.starting_area_item = re_gen_passthrough["starting_area"]
for task_type in task_types:
if f"max_{task_type}_level" in re_gen_passthrough:
getattr(self.options,f"max_{task_type}_level").value = re_gen_passthrough[f"max_{task_type}_level"]
max_count = getattr(self.options,f"max_{task_type}_tasks")
max_count.value = max_count.range_end
self.options.brutal_grinds.value = re_gen_passthrough["brutal_grinds"]
""" """
This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client.
@@ -115,20 +128,13 @@ class OSRSWorld(World):
data = self.options.as_dict("brutal_grinds") data = self.options.as_dict("brutal_grinds")
data["data_csv_tag"] = data_csv_tag data["data_csv_tag"] = data_csv_tag
data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv
for task_type in task_types:
data[f"max_{task_type}_level"] = getattr(self.options,f"max_{task_type}_level").value
return data return data
def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: @staticmethod
if "starting_area" in slot_data: def interpret_slot_data(slot_data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
self.starting_area_item = slot_data["starting_area"] return slot_data
menu_region = self.multiworld.get_region("Menu",self.player)
menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
starting_area_region = self.starting_area_item[6:] # len("Area: ")
starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
def create_regions(self) -> None: def create_regions(self) -> None:
""" """
@@ -195,6 +201,8 @@ class OSRSWorld(World):
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
locations_required = 0 locations_required = 0
for item_row in item_rows: for item_row in item_rows:
if item_row.name == self.starting_area_item:
continue #skip starting area
# If it's a filler item, set it aside for later # If it's a filler item, set it aside for later
if item_row.progression == ItemClassification.filler: if item_row.progression == ItemClassification.filler:
continue continue
@@ -206,15 +214,18 @@ class OSRSWorld(World):
locations_required += item_row.amount locations_required += item_row.amount
if self.options.enable_duds: locations_required += self.options.dud_count if self.options.enable_duds: locations_required += self.options.dud_count
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 locations_added = 0 # Keep track of the number of locations we add so we don't add more the number of items we're going to make
# Quests are always added first, before anything else is rolled # Quests are always added first, before anything else is rolled
for i, location_row in enumerate(location_rows): for i, location_row in enumerate(location_rows):
if location_row.category in {"quest", "points", "goal"}: if location_row.category in {"quest"}:
if self.task_within_skill_levels(location_row.skills): if self.task_within_skill_levels(location_row.skills):
self.create_and_add_location(i) self.create_and_add_location(i)
if location_row.category == "quest": locations_added += 1
locations_added += 1 elif location_row.category in {"goal"}:
if not self.task_within_skill_levels(location_row.skills):
raise OptionError(f"Goal location for {self.player_name} not allowed in skill levels") #it doesn't actually have any, but just in case for future
self.create_and_add_location(i)
# Build up the weighted Task Pool # Build up the weighted Task Pool
rnd = self.random rnd = self.random
@@ -225,18 +236,28 @@ class OSRSWorld(World):
rnd.shuffle(general_tasks) rnd.shuffle(general_tasks)
else: else:
general_tasks.reverse() general_tasks.reverse()
for i in range(self.options.minimum_general_tasks): general_tasks_added = 0
while general_tasks_added<self.options.minimum_general_tasks and general_tasks:
task = general_tasks.pop() task = general_tasks.pop()
self.add_location(task) if self.task_within_skill_levels(task.skills):
locations_added += 1 self.add_location(task)
locations_added += 1
general_tasks_added += 1
while generation_is_fake and len(general_tasks)>0:
task = general_tasks.pop()
if self.task_within_skill_levels(task.skills):
self.add_location(task)
locations_added += 1
general_tasks_added += 1
if general_tasks_added < self.options.minimum_general_tasks:
raise OptionError(f"{self.plyaer_name} doesn't have enough general tasks to create required minimum count"+
f", raise maximum skill levels or lower minimum general tasks")
general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0 general_weight = self.options.general_task_weight.value if len(general_tasks) > 0 else 0
tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {} tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {}
weights_per_task_type: typing.Dict[str, int] = {} weights_per_task_type: typing.Dict[str, int] = {}
task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
for task_type in task_types: for task_type in task_types:
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
tasks_for_this_type = [task for task in self.locations_by_category[task_type] tasks_for_this_type = [task for task in self.locations_by_category[task_type]
@@ -263,10 +284,13 @@ class OSRSWorld(World):
all_weights.append(weights_per_task_type[task_type]) all_weights.append(weights_per_task_type[task_type])
# Even after the initial forced generals, they can still be rolled randomly # Even after the initial forced generals, they can still be rolled randomly
if general_weight > 0: if general_weight > 0 and len(general_tasks)>0:
all_tasks.append(general_tasks) all_tasks.append(general_tasks)
all_weights.append(general_weight) all_weights.append(general_weight)
if not generation_is_fake and locations_added > locations_required: #due to minimum general tasks we already have more than needed
raise OptionError(f"Too many locations created for {self.player_name}, lower the minimum general tasks")
while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0): while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0):
if all_tasks: if all_tasks:
chosen_task = rnd.choices(all_tasks, all_weights)[0] chosen_task = rnd.choices(all_tasks, all_weights)[0]
@@ -282,9 +306,9 @@ class OSRSWorld(World):
del all_tasks[index] del all_tasks[index]
del all_weights[index] del all_weights[index]
else: else: # We can ignore general tasks in UT because they will have been cleared already
if len(general_tasks) == 0: if len(general_tasks) == 0:
raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " + raise OptionError(f"There are not enough available tasks to fill the remaining pool for OSRS " +
f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.") f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.")
task = general_tasks.pop() task = general_tasks.pop()
self.add_location(task) self.add_location(task)
@@ -296,7 +320,7 @@ class OSRSWorld(World):
self.create_and_add_location(index) self.create_and_add_location(index)
def create_items(self) -> None: def create_items(self) -> None:
filler_items = [] filler_items:list[ItemRow] = []
for item_row in item_rows: for item_row in item_rows:
if item_row.name != self.starting_area_item: if item_row.name != self.starting_area_item:
# If it's a filler item, set it aside for later # If it's a filler item, set it aside for later
@@ -321,7 +345,7 @@ class OSRSWorld(World):
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
if self.options.enable_duds: if self.options.enable_duds:
return self.random.choice([item for item in item_rows if item.progression == ItemClassification.filler]) return self.random.choice([item.name for item in item_rows if item.progression == ItemClassification.filler])
else: else:
return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic, return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor, ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor,
@@ -388,6 +412,12 @@ class OSRSWorld(World):
# Set the access rule for the QP Location # Set the access rule for the QP Location
add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state)))
qp = 0
for qp_event in self.available_QP_locations:
qp += int(qp_event[0])
if qp < self.location_rows_by_name[LocationNames.Q_Dragon_Slayer].qp:
raise OptionError(f"{self.player_name} doesn't have enough quests for reach goal, increase maximum skill levels")
# place "Victory" at "Dragon Slayer" and set collection as win condition # place "Victory" at "Dragon Slayer" and set collection as win condition
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \
.place_locked_item(self.create_event("Victory")) .place_locked_item(self.create_event("Victory"))