From 1b402874e131c656cc03ec656a3aa5e8167afdce Mon Sep 17 00:00:00 2001 From: Anilyka Barry Date: Tue, 16 Jun 2026 16:25:51 -0400 Subject: [PATCH 1/4] First draft for Tradesanity --- worlds/crosscode/codegen/lists.py | 19 +++++++++++++++++++ worlds/crosscode/types/traders.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 worlds/crosscode/types/traders.py diff --git a/worlds/crosscode/codegen/lists.py b/worlds/crosscode/codegen/lists.py index 1b5f254a35c8..d6f57aaa5dc7 100644 --- a/worlds/crosscode/codegen/lists.py +++ b/worlds/crosscode/codegen/lists.py @@ -17,6 +17,7 @@ from ..types.locations import AccessInfo, LocationData from ..types.condition import Condition, NeverCondition, RegionCondition, OrCondition, AndCondition, ShopSlotCondition from ..types.shops import ShopData +from ..types.traders import TraderData, SingleTrade class LocationCategory(StrEnum): """ @@ -66,6 +67,14 @@ class ListInfo: shop_unlock_by_shop_and_id: dict[tuple[str, int], ItemPoolEntry] global_slot_region_conditions_list: dict[str, list[Condition]] + trader_data: dict[str, TraderData] + per_trader_locations: dict[str, dict[int, LocationData]] + global_trader_locations: dict[int, LocationData] + trader_unlock_by_id: dict[int, ItemPoolEntry] + trader_unlock_by_trader: dict[str, ItemPoolEntry] + trader_unlock_by_trader_and_id: dict[tuple[str, int], ItemPoolEntry] + global_trade_region_conditions_list: dict[str, list[Condition]] + region_botanics_amounts: dict[str, dict[str, int]] # { mode => { region => number of plants } } botanics_internal_names_to_ids: dict[str, int] @@ -462,6 +471,16 @@ def __add_shop_list(self, loc_list: dict[str, dict[str, typing.Any]]): for name, raw_shop in loc_list.items(): self.__add_shop(name, raw_shop) + def __add_trader(self, internal_name: str, raw_trader: dict[str, typing.Any]): + trader_name: str = raw_trader["location"]["trader"] + area = raw_trader["location"]["area"] + area_name = self.ctx.area_names[area] + + metadata = raw_trader["metadata"] + metadata["trader"] = True + + access_info = self.json_parser.parse_location_access_info(raw_trader) + def __add_item_data_list(self, item_list: dict[str, dict[str, typing.Any]]): """ Add a list of items to the list. diff --git a/worlds/crosscode/types/traders.py b/worlds/crosscode/types/traders.py new file mode 100644 index 000000000000..4d65774cdfb5 --- /dev/null +++ b/worlds/crosscode/types/traders.py @@ -0,0 +1,21 @@ +import typing +from dataclasses import dataclass + +from .locations import AccessInfo +from .metadata import IncludeOptions +from .items import ItemData + +@dataclass +class TraderData: + internal_name: str + name: str + access: AccessInfo + metadata: typing.Optional[IncludeOptions] = None + +@dataclass +class SingleTrade: + display_name: str + reward: ItemData + cost: list[ItemData] + index: int + From 704ca65f85ab472d78935f5c5bef61916de88cd3 Mon Sep 17 00:00:00 2001 From: Anilyka Barry Date: Wed, 17 Jun 2026 00:00:38 -0400 Subject: [PATCH 2/4] Add more logic to the trader handling --- worlds/crosscode/codegen/lists.py | 89 ++++++++++++++++++++++++++++- worlds/crosscode/types/condition.py | 9 +++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/worlds/crosscode/codegen/lists.py b/worlds/crosscode/codegen/lists.py index d6f57aaa5dc7..e8714093dee2 100644 --- a/worlds/crosscode/codegen/lists.py +++ b/worlds/crosscode/codegen/lists.py @@ -15,7 +15,11 @@ from ..types.items import ItemData, ProgressiveItemChainSingle, SingleItemData, ItemPoolEntry, ProgressiveItemChain from ..types.locations import AccessInfo, LocationData -from ..types.condition import Condition, NeverCondition, RegionCondition, OrCondition, AndCondition, ShopSlotCondition +from ..types.condition import ( + Condition, NeverCondition, RegionCondition, + OrCondition, AndCondition, ShopSlotCondition, + SingleTradeCondition, +) from ..types.shops import ShopData from ..types.traders import TraderData, SingleTrade @@ -121,6 +125,14 @@ def __init__(self, ctx: Context): self.shop_unlock_by_shop_and_id = {} self.global_slot_region_conditions_list = {} + self.trader_data = {} + self.per_trader_locations = defaultdict(dict) + self.global_trader_locations = {} + self.trader_unlock_by_id = {} + self.trader_unlock_by_trader = {} + self.trader_unlock_by_trader_and_id = {} + self.global_trade_region_conditions_list = {} + self.region_botanics_amounts = defaultdict(lambda: defaultdict(lambda: 0)) self.botanics_internal_names_to_ids = {} @@ -472,6 +484,7 @@ def __add_shop_list(self, loc_list: dict[str, dict[str, typing.Any]]): self.__add_shop(name, raw_shop) def __add_trader(self, internal_name: str, raw_trader: dict[str, typing.Any]): + # is it possible to simplify this code by combining it with the shop code? trader_name: str = raw_trader["location"]["trader"] area = raw_trader["location"]["area"] area_name = self.ctx.area_names[area] @@ -481,6 +494,80 @@ def __add_trader(self, internal_name: str, raw_trader: dict[str, typing.Any]): access_info = self.json_parser.parse_location_access_info(raw_trader) + # it's empty, but since it's a defaultdict[list], we get an empty list + # so we can modify it in-place and it'll get reflected + locs = self.per_trader_locations[trader_name] + + unlock = f"Trader Unlock: {trader_name}" + # the desired behaviour is identical for shops and traders, so can reuse + unlock_item = self.__add_shop_unlock_item(unlock) + if trader_name not in self.trader_unlock_by_trader: + self.trader_unlock_by_trader[trader_name] = ItemPoolEntry(unlock_item, 1, metadata) + self.descriptions[unlock_item.combo_id] = { + # oh no the french are invading + "en_US": fr"Unlocks \c[3]all trades\c[0] for trader \c[3]{trader_name}\c[0]." + } + + trader_unlocks = self.item_groups.setdefault("Trader Unlocks", []) + if unlock_item not in trader_unlocks: + trader_unlocks.append(unlock_item) + + global_item_group = self.item_groups.setdefault("Global Trader Unlocks", []) + trader_area_group = self.item_groups.setdefault(f"Trader Unlocks: {area_name}", []) + + trade_unlocks_group = self.item_groups.setdefault("Trade Unlocks", []) + this_trader_unlocks = self.item_groups.setdefault(f"Trade Unlocks: {trader_name}", []) + + for trade_name, trade in raw_trader["trades"].items(): + t_, name, count = trade["reward"] + item_data = self.ctx.rando_data["items"][name] + if t_ != "item": # all trades should give an item + continue + + trade_loc_name = f"Trade: {trade_name} ({trader_name})" + locid = self.__get_or_allocate_location_id(trade_loc_name) + + item_id = item_data["id"] + + trade_location = LocationData( + name=trade_loc_name, + code=locid, + area=area, + metadata=metadata, + access=AccessInfo( + region={rn: trader_name for rn in access_info.region}, + cond=[SingleTradeCondition(trader_name, item_id)] + ) + ) + + self.location_groups[area_name].append(trade_location) + self.location_groups[f"{area_name} Traders"].append(trade_location) + + locs[item_id] = trade_location + self.locations_data[trade_location.name] = trade_location + + global_location = self.global_trader_locations.get(item_id) + + by_trader_and_id_name = f"Trade Unlock: {trade_name} ({trader_name})" + by_trader_and_id_item = self.__add_shop_unlock_item(by_trader_and_id_name) + self.trader_unlock_by_trader_and_id[internal_name, item_id] + + self.descriptions[by_trader_and_id_item.combo_id] = { + "en_US": fr"Unlocks the trade \c[3]{trade_name}\c[0] by trader \c[3]{trader_name}\c[0]." + } + + # TODO: finish this method + # the goal is to have four lists: + # - every trader + # - every trader in a given area + # - every trade + # - every trade for a given trader + # for the area, we want the logically-grouped ones, e.g. "Autumn's Rise" + # which would include every trader in Obelisk, but not those in Rookie Harbor + # it would also be nice to add that to shops, but that's for later + + + def __add_item_data_list(self, item_list: dict[str, dict[str, typing.Any]]): """ Add a list of items to the list. diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index dc383b4396fa..a9d4c7bcb1bd 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -167,6 +167,15 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a return state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].item.name, player) return True +@dataclass +class SingleTradeCondition(Condition): + trader_name: str + item_id: int + + def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + # XXX: need to implement this as tradesanity gets further implemented + return True + @dataclass class BotanicsCompletionCondition(Condition): amount: float From 2b7721b04af0c810100152c0d059c8de606e8e0d Mon Sep 17 00:00:00 2001 From: Anilyka Barry Date: Thu, 30 Jul 2026 19:10:53 -0400 Subject: [PATCH 3/4] Finish(?) the baseline integration --- worlds/crosscode/codegen/lists.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/worlds/crosscode/codegen/lists.py b/worlds/crosscode/codegen/lists.py index e8714093dee2..8f5ffe986232 100644 --- a/worlds/crosscode/codegen/lists.py +++ b/worlds/crosscode/codegen/lists.py @@ -515,6 +515,9 @@ def __add_trader(self, internal_name: str, raw_trader: dict[str, typing.Any]): global_item_group = self.item_groups.setdefault("Global Trader Unlocks", []) trader_area_group = self.item_groups.setdefault(f"Trader Unlocks: {area_name}", []) + global_item_group.append(unlock_item) + trader_area_group.append(unlock_item) + trade_unlocks_group = self.item_groups.setdefault("Trade Unlocks", []) this_trader_unlocks = self.item_groups.setdefault(f"Trade Unlocks: {trader_name}", []) @@ -543,31 +546,20 @@ def __add_trader(self, internal_name: str, raw_trader: dict[str, typing.Any]): self.location_groups[area_name].append(trade_location) self.location_groups[f"{area_name} Traders"].append(trade_location) + trade_unlocks_group.append(trade_location) + this_trader_unlocks.append(trade_location) + locs[item_id] = trade_location self.locations_data[trade_location.name] = trade_location - global_location = self.global_trader_locations.get(item_id) - by_trader_and_id_name = f"Trade Unlock: {trade_name} ({trader_name})" by_trader_and_id_item = self.__add_shop_unlock_item(by_trader_and_id_name) self.trader_unlock_by_trader_and_id[internal_name, item_id] self.descriptions[by_trader_and_id_item.combo_id] = { - "en_US": fr"Unlocks the trade \c[3]{trade_name}\c[0] by trader \c[3]{trader_name}\c[0]." + "en_US": fr"Unlocks the trade \c[3]{trade_name}\c[0] from trader \c[3]{trader_name}\c[0]." } - # TODO: finish this method - # the goal is to have four lists: - # - every trader - # - every trader in a given area - # - every trade - # - every trade for a given trader - # for the area, we want the logically-grouped ones, e.g. "Autumn's Rise" - # which would include every trader in Obelisk, but not those in Rookie Harbor - # it would also be nice to add that to shops, but that's for later - - - def __add_item_data_list(self, item_list: dict[str, dict[str, typing.Any]]): """ Add a list of items to the list. From ee4e3383a961489c66a82b117c16f21e572185d5 Mon Sep 17 00:00:00 2001 From: Anilyka Barry Date: Thu, 30 Jul 2026 19:34:27 -0400 Subject: [PATCH 4/4] Add missing trader data --- worlds/crosscode/codegen/lists.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/worlds/crosscode/codegen/lists.py b/worlds/crosscode/codegen/lists.py index 8f5ffe986232..8e3afa90304c 100644 --- a/worlds/crosscode/codegen/lists.py +++ b/worlds/crosscode/codegen/lists.py @@ -127,11 +127,10 @@ def __init__(self, ctx: Context): self.trader_data = {} self.per_trader_locations = defaultdict(dict) - self.global_trader_locations = {} - self.trader_unlock_by_id = {} + self.trader_unlock_by_id = {} # unused? self.trader_unlock_by_trader = {} self.trader_unlock_by_trader_and_id = {} - self.global_trade_region_conditions_list = {} + self.global_trade_region_conditions_list = {} # ? self.region_botanics_amounts = defaultdict(lambda: defaultdict(lambda: 0)) self.botanics_internal_names_to_ids = {} @@ -560,6 +559,13 @@ def __add_trader(self, internal_name: str, raw_trader: dict[str, typing.Any]): "en_US": fr"Unlocks the trade \c[3]{trade_name}\c[0] from trader \c[3]{trader_name}\c[0]." } + self.trader_data[trader_name] = TraderData( + internal_name=internal_name, + name=trader_name, + access=access_info, + metadata=metadata, + ) + def __add_item_data_list(self, item_list: dict[str, dict[str, typing.Any]]): """ Add a list of items to the list.