From 41ce4c4f77f50c8cbdceeeb8cd576aae9079ce54 Mon Sep 17 00:00:00 2001
From: frenkey-derp <54779599+frenkey-derp@users.noreply.github.com>
Date: Sun, 31 May 2026 16:14:09 +0200
Subject: [PATCH] ItemData, Snapshot and Upgrades updated
---
Py4GWCoreLib/item_data/ItemData.py | 606 ++++++++++++++++++
Py4GWCoreLib/item_data/item_snapshot.py | 484 ++++++++++++++
.../item_mods_src/item_modifier_parser.py | 4 +-
Py4GWCoreLib/item_mods_src/upgrade_parser.py | 318 ++++-----
Py4GWCoreLib/item_mods_src/upgrades.py | 17 +-
.../native_src/internals/encoded_strings.py | 120 +++-
.../native_src/internals/string_table.py | 2 +-
7 files changed, 1362 insertions(+), 189 deletions(-)
create mode 100644 Py4GWCoreLib/item_data/ItemData.py
create mode 100644 Py4GWCoreLib/item_data/item_snapshot.py
diff --git a/Py4GWCoreLib/item_data/ItemData.py b/Py4GWCoreLib/item_data/ItemData.py
new file mode 100644
index 000000000..94a5d93fe
--- /dev/null
+++ b/Py4GWCoreLib/item_data/ItemData.py
@@ -0,0 +1,606 @@
+# singleton instance which contains all the item data of our `items.json` file. This is used to avoid having to read the file multiple times and to have a central place to access item data from
+
+from dataclasses import dataclass, field
+from datetime import datetime, timedelta, date
+import json
+import os
+import traceback
+from typing import Optional
+
+from Py4GW import Console
+
+from Py4GWCoreLib.GlobalCache import GLOBAL_CACHE
+from Py4GWCoreLib.Player import Player
+from Py4GWCoreLib.enums_src.GameData_enums import Attribute, Profession
+from Py4GWCoreLib.enums_src.Item_enums import ItemType, NICK_CYCLE_COUNT, NICK_CYCLE_START_DATE
+from Py4GWCoreLib.enums_src.Model_enums import ModelID
+from Py4GWCoreLib.enums_src.Multiboxing_enums import ReloadType, SharedCommandType
+from Py4GWCoreLib.enums_src.Region_enums import ServerLanguage
+from Py4GWCoreLib.native_src.internals.encoded_strings import GWStringEncoded
+
+PERSISTENT = True
+
+@dataclass
+class SalvageInfo():
+ amount: int = -1
+ min_amount: int = -1
+ max_amount: int = -1
+ model_id: int = -1
+ name: str = ""
+ summary: str = ""
+ average_amount: float = 0
+
+ def __post_init__(self):
+ self.generate_summary()
+
+ def generate_summary(self):
+ amount = f"{self.amount}" if self.amount != -1 else f"{self.min_amount} - {self.max_amount}" if self.min_amount != -1 and self.max_amount != -1 else None
+ self.summary = f"{amount} {self.name}" if amount else self.name
+
+
+ def to_dict(self) -> dict:
+ return {
+ "amount": self.amount,
+ "min_amount": self.min_amount,
+ "max_amount": self.max_amount,
+ "model_id": self.model_id,
+ "name": self.name
+ }
+
+ def __str__(self) -> str:
+ return f"SalvageInfo(amount={self.amount}, min_amount={self.min_amount}, max_amount={self.max_amount}, model_id={self.model_id}, name='{self.name}')"
+
+ def __repr__(self) -> str:
+ return f"SalvageInfo(amount={self.amount}, min_amount={self.min_amount}, max_amount={self.max_amount}, model_id={self.model_id}, name='{self.name}')"
+
+ @staticmethod
+ def from_dict(data: dict) -> 'SalvageInfo':
+ info = SalvageInfo()
+ info.amount = data.get("amount", -1)
+ info.min_amount = data.get("min_amount", -1)
+ info.max_amount = data.get("max_amount", -1)
+ info.model_id = data.get("model_id", -1)
+ info.name = data.get("name", "")
+ info.generate_summary()
+
+ return info
+
+ def update_from(self, other: object) -> bool:
+ """
+ Update the current SalvageInfo with information from another SalvageInfo instance.
+ Only updates fields that are missing or empty in the current instance.
+
+ Args:
+ other (SalvageInfo): The other SalvageInfo instance to update from.
+
+ Returns:
+ bool: True if any changes were made, False otherwise.
+ """
+ changed = False
+ if not isinstance(other, SalvageInfo):
+ return False
+
+ if self.amount != other.amount and other.amount != -1:
+ self.amount = other.amount
+ changed = True
+
+ if self.min_amount != other.min_amount and other.min_amount != -1:
+ self.min_amount = other.min_amount
+ changed = True
+
+ if self.max_amount != other.max_amount and other.max_amount != -1:
+ self.max_amount = other.max_amount
+ changed = True
+
+ if self.model_id != other.model_id and other.model_id != -1:
+ self.model_id = other.model_id
+ changed = True
+
+ if self.name != other.name and other.name:
+ self.name = other.name
+ changed = True
+
+ if changed:
+ self.generate_summary()
+
+ return changed
+
+class SalvageInfoCollection(dict[str, 'SalvageInfo']):
+ """
+ A collection of SalvageInfo objects indexed by material name.
+
+ This class extends the built-in dict to provide a more specific type for
+ collections of SalvageInfo objects.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def to_dict(self) -> dict:
+ return {material_name: salvage_info.to_dict() for material_name, salvage_info in self.items()}
+
+ @staticmethod
+ def from_dict(data: dict) -> 'SalvageInfoCollection':
+ """
+ Create a SalvageInfoCollection from a dictionary.
+
+ Args:
+ data (dict): A dictionary where keys are material names and values are SalvageInfo dictionaries.
+
+ Returns:
+ SalvageInfoCollection: An instance of SalvageInfoCollection populated with the data.
+ """
+ collection = SalvageInfoCollection()
+
+ for material_name, salvage_info_data in data.items():
+ collection[material_name] = SalvageInfo.from_dict(salvage_info_data)
+
+ return collection
+
+ def update_from(self, other: object) -> bool:
+ """
+ Update the current SalvageInfoCollection with information from another SalvageInfoCollection instance.
+ Only updates fields that are missing or empty in the current instance.
+
+ Args:
+ other (SalvageInfoCollection): The other SalvageInfoCollection instance to update from.
+
+ Returns:
+ bool: True if any changes were made, False otherwise.
+ """
+ changed = False
+ if not isinstance(other, SalvageInfoCollection):
+ return False
+
+ for material_name, other_info in other.items():
+ if material_name in self:
+ if self[material_name].update_from(other_info):
+ changed = True
+ else:
+ self[material_name] = other_info
+ changed = True
+
+ return changed
+
+@dataclass(eq=False)
+class ItemData:
+ model_id: int = -1
+ item_type: ItemType = ItemType.Unknown
+ model_file_id: int = -1
+ english_name: str = ""
+ name_encoded : bytes = bytes()
+ attributes: list[Attribute] = field(default_factory=list)
+ common_salvage: Optional[SalvageInfoCollection] = field(default_factory=SalvageInfoCollection)
+ rare_salvage: Optional[SalvageInfoCollection] = field(default_factory=SalvageInfoCollection)
+ nick_index: Optional[int] = None
+ profession : Optional[Profession] = None
+
+ # Optional fields we have to more or less manually fill/scrape
+ wiki_url: str = ""
+ acquisition: str = ""
+ description: str = ""
+ category: str = ""
+ sub_category: str = ""
+
+ skin: Optional[str] = None
+
+ _names: GWStringEncoded = GWStringEncoded(b"", "Unknown Item")
+
+ def __post_init__(self):
+ self._update_names()
+ self.nick_date = self.next_nick_week
+
+ def __setattr__(self, name, value):
+ super().__setattr__(name, value)
+
+ if name == "name_encoded" or name == "english_name":
+ self._update_names()
+
+ def __hash__(self) -> int:
+ return hash((self.model_id, self.item_type))
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, ItemData):
+ return NotImplemented
+
+ return self.model_id == other.model_id and self.item_type == other.item_type
+
+ def get_next_nick_cycle_start_date(self, start_date: Optional[datetime] = None) -> Optional[date]:
+ if self.nick_index is None:
+ return None
+
+ start_date = start_date or datetime.now()
+ nick_start_date = datetime.combine(NICK_CYCLE_START_DATE, datetime.min.time())
+
+ # Monday 00:00 of the week we want to evaluate from.
+ monday_of_current_week = start_date.date() - timedelta(days=start_date.weekday())
+ current_week_start = datetime.combine(monday_of_current_week, datetime.min.time())
+
+ first_matching_cycle = nick_start_date + timedelta(weeks=self.nick_index)
+ if first_matching_cycle >= current_week_start:
+ return first_matching_cycle.date()
+
+ weeks_since_first_match = (current_week_start - first_matching_cycle).days // 7
+ cycles_to_advance = (weeks_since_first_match + NICK_CYCLE_COUNT - 1) // NICK_CYCLE_COUNT
+ next_cycle = first_matching_cycle + timedelta(weeks=cycles_to_advance * NICK_CYCLE_COUNT)
+ return next_cycle.date()
+
+ def get_weeks_until_next_nick(self, start_date: Optional[datetime] = None) -> Optional[int]:
+ next_nick_week = self.get_next_nick_cycle_start_date(start_date)
+ if next_nick_week is None:
+ return None
+
+ start_date = start_date or datetime.now()
+ monday_of_current_week = start_date.date() - timedelta(days=start_date.weekday())
+ current_week_start = datetime.combine(monday_of_current_week, datetime.min.time()).date()
+ delta = next_nick_week - current_week_start
+ if delta.days < 0:
+ return None
+
+ return delta.days // 7
+
+ @property
+ def next_nick_week(self) -> Optional[date]:
+ return self.get_next_nick_cycle_start_date()
+
+ @property
+ def weeks_until_next_nick(self) -> Optional[int]:
+ return self.get_weeks_until_next_nick()
+
+ def _update_names(self):
+ self._names = GWStringEncoded(self.name_encoded or bytes(), self.english_name or "Unknown Item")
+
+ @property
+ def names(self) -> GWStringEncoded:
+ return self._names
+
+ @property
+ def name(self) -> str:
+ return self._names.plain_singular
+
+ @property
+ def plural_name(self) -> str:
+ return self._names.with_amount(2)
+
+ @staticmethod
+ def _parse_name_encoded(value: Optional[str]) -> bytes:
+ if not value:
+ return bytes()
+
+ value = value.strip()
+ if not value:
+ return bytes()
+
+ parts = [part.strip() for part in value.split(",") if part.strip()]
+ return bytes(int(part, 16) for part in parts)
+
+ @staticmethod
+ def _format_name_encoded(value: bytes) -> str:
+ if not value:
+ return ""
+
+ return ", ".join(f"0x{byte:X}" for byte in value)
+
+ @staticmethod
+ def from_dict(data: dict) -> 'ItemData':
+ profession_name = data.get("profession")
+ english_name = data.get("name", "")
+
+ item_data = ItemData(
+ english_name=english_name,
+ name_encoded=ItemData._parse_name_encoded(data.get("name_encoded")),
+ model_id=data.get("model_id", -1),
+ item_type=ItemType[data.get("item_type", "Unknown")],
+ model_file_id=data.get("model_file_id", -1),
+ attributes=[Attribute[attr] for attr in data.get("attributes", [])],
+ common_salvage=SalvageInfoCollection.from_dict(data.get("common_salvage", {})) if data.get("common_salvage") else None,
+ rare_salvage=SalvageInfoCollection.from_dict(data.get("rare_salvage", {})) if data.get("rare_salvage") else None,
+ nick_index=data.get("nick_index"),
+ profession=Profession[profession_name] if profession_name else None,
+ wiki_url=data.get("wiki_url", ""),
+ acquisition=data.get("acquisition", ""),
+ description=data.get("description", ""),
+ category=data.get("category", ""),
+ sub_category=data.get("sub_category", ""),
+ skin=data.get("skin")
+ )
+
+ return item_data
+
+ def to_dict(self) -> dict:
+ data = {
+ "model_id": self.model_id,
+ "item_type": self.item_type.name,
+ "model_file_id": self.model_file_id,
+ "name": self.english_name,
+ "name_encoded" : self._format_name_encoded(self.name_encoded),
+ "attributes": [attr.name for attr in self.attributes],
+ "common_salvage": self.common_salvage.to_dict() if self.common_salvage else None,
+ "rare_salvage": self.rare_salvage.to_dict() if self.rare_salvage else None,
+ "nick_index": self.nick_index,
+ "profession": self.profession.name if self.profession else None,
+ "wiki_url": self.wiki_url,
+ "acquisition": self.acquisition,
+ "description": self.description,
+ "category": self.category,
+ "sub_category": self.sub_category,
+ "skin": self.skin
+ }
+
+ return dict(sorted(data.items(), key=lambda item: item[0]))
+
+ def update_from(self, other: object) -> bool:
+ """
+ Update the current ItemData with information from another ItemData instance.
+ Only updates fields that are missing or empty in the current instance.
+
+ Args:
+ other (ItemData): The other ItemData instance to update from.
+
+ Returns:
+ bool: True if any changes were made, False otherwise.
+ """
+ changed = False
+ if not isinstance(other, ItemData):
+ return False
+
+ if self.model_id == -1 and other.model_id != -1:
+ self.model_id = other.model_id
+ changed = True
+
+ if self.item_type == ItemType.Unknown and other.item_type != ItemType.Unknown:
+ self.item_type = other.item_type
+ changed = True
+
+ if not self.english_name and other.english_name:
+ self.english_name = other.english_name
+ changed = True
+
+ if not self.name_encoded and other.name_encoded:
+ self.name_encoded = other.name_encoded
+ changed = True
+
+ merged_attributes = sorted(set(self.attributes).union(other.attributes), key=lambda attr: attr.name)
+ if merged_attributes != self.attributes:
+ self.attributes = merged_attributes
+ changed = True
+
+ if self.common_salvage is None and other.common_salvage is not None:
+ self.common_salvage = other.common_salvage
+ changed = True
+ elif self.common_salvage is not None and other.common_salvage is not None:
+ if self.common_salvage.update_from(other.common_salvage):
+ changed = True
+
+ if self.rare_salvage is None and other.rare_salvage is not None:
+ self.rare_salvage = other.rare_salvage
+ changed = True
+ elif self.rare_salvage is not None and other.rare_salvage is not None:
+ if self.rare_salvage.update_from(other.rare_salvage):
+ changed = True
+
+ if self.nick_index is None and other.nick_index is not None:
+ self.nick_index = other.nick_index
+ changed = True
+
+ if self.profession is None and other.profession is not None:
+ self.profession = other.profession
+ changed = True
+
+ if not self.wiki_url and other.wiki_url:
+ self.wiki_url = other.wiki_url
+ changed = True
+
+ if not self.acquisition and other.acquisition:
+ self.acquisition = other.acquisition
+ changed = True
+
+ if not self.description and other.description:
+ self.description = other.description
+ changed = True
+
+ if not self.category and other.category:
+ self.category = other.category
+ changed = True
+
+ if not self.sub_category and other.sub_category:
+ self.sub_category = other.sub_category
+ changed = True
+
+ if self.skin is None and other.skin:
+ self.skin = other.skin
+ changed = True
+
+ return changed
+
+DAMAGE_RANGES : dict[ItemType, dict[int, tuple[int, int]]] = {
+ ItemType.Axe: {
+ 0: (6, 12),
+ 1: (6, 12),
+ 2: (6, 14),
+ 3: (6, 17),
+ 4: (6, 19),
+ 5: (6, 22),
+ 6: (6, 24),
+ 7: (6, 25),
+ 8: (6, 27),
+ 9: (6, 28),
+ },
+
+ ItemType.Bow: {
+ 0: (9, 13),
+ 1: (9, 14),
+ 2: (10, 16),
+ 3: (11, 18),
+ 4: (12, 20),
+ 5: (13, 22),
+ 6: (14, 25),
+ 7: (14, 25),
+ 8: (14, 27),
+ 9: (14, 28),
+ },
+
+ ItemType.Daggers: {
+ 0: (4, 8),
+ 1: (4, 8),
+ 2: (5, 9),
+ 3: (5, 11),
+ 4: (6, 12),
+ 5: (6, 13),
+ 6: (7, 14),
+ 7: (7, 15),
+ 8: (7, 16),
+ 9: (7, 17),
+ },
+
+ ItemType.Offhand: {
+ 0: (6, 6),
+ 1: (6, 6),
+ 2: (7, 7),
+ 3: (8, 8),
+ 4: (9, 9),
+ 5: (10, 10),
+ 6: (11, 11),
+ 7: (11, 11),
+ 8: (12, 12),
+ 9: (12, 12),
+ },
+
+ ItemType.Hammer: {
+ 0: (11, 15),
+ 1: (11, 16),
+ 2: (12, 19),
+ 3: (14, 22),
+ 4: (15, 24),
+ 5: (16, 28),
+ 6: (17, 30),
+ 7: (18, 32),
+ 8: (18, 34),
+ 9: (19, 35),
+ },
+
+ ItemType.Scythe: {
+ 0: (8, 17),
+ 1: (8, 18),
+ 2: (9, 21),
+ 3: (10, 24),
+ 4: (10, 28),
+ 5: (10, 32),
+ 6: (10, 35),
+ 7: (10, 36),
+ 8: (9, 40),
+ 9: (9, 41),
+ },
+
+ ItemType.Shield: {
+ 0: (8, 8),
+ 1: (9, 9),
+ 2: (10, 10),
+ 3: (11, 11),
+ 4: (12, 12),
+ 5: (13, 13),
+ 6: (14, 14),
+ 7: (15, 15),
+ 8: (16, 16),
+ 9: (16, 16),
+ },
+
+ ItemType.Spear: {
+ 0: (8, 12),
+ 1: (8, 13),
+ 2: (10, 15),
+ 3: (11, 17),
+ 4: (11, 19),
+ 5: (12, 21),
+ 6: (13, 23),
+ 7: (13, 25),
+ 8: (14, 26),
+ 9: (14, 27),
+ },
+
+ ItemType.Staff: {
+ 0: (7, 11),
+ 1: (7, 11),
+ 2: (8, 13),
+ 3: (9, 14),
+ 4: (10, 16),
+ 5: (10, 18),
+ 6: (10, 19),
+ 7: (11, 20),
+ 8: (11, 21),
+ 9: (11, 22),
+ },
+
+ ItemType.Sword: {
+ 0: (8, 10),
+ 1: (8, 11),
+ 2: (9, 13),
+ 3: (11, 14),
+ 4: (12, 16),
+ 5: (13, 18),
+ 6: (14, 19),
+ 7: (14, 20),
+ 8: (15, 22),
+ 9: (15, 22),
+ },
+
+ ItemType.Wand: {
+ 0: (7, 11),
+ 1: (7, 11),
+ 2: (8, 13),
+ 3: (9, 14),
+ 4: (10, 16),
+ 5: (10, 18),
+ 6: (11, 19),
+ 7: (11, 20),
+ 8: (11, 21),
+ 9: (11, 22),
+ },
+}
+
+MATERIAL_STORAGE_SLOTS : dict[int, int] = {
+ ModelID.Bone: 0, # Bone
+ ModelID.Lump_Of_Charcoal: 26, # Lump of Charcoal
+ ModelID.Monstrous_Claw: 19, # Monstrous Claw
+ ModelID.Bolt_Of_Cloth: 5, # Bolt of Cloth
+ ModelID.Bolt_Of_Linen: 13, # Bolt of Linen
+ ModelID.Bolt_Of_Damask: 14, # Bolt of Damask
+ ModelID.Bolt_Of_Silk: 15, # Bolt of Silk
+ ModelID.Pile_Of_Glittering_Dust: 9, # Pile of Glittering Dust
+ ModelID.Glob_Of_Ectoplasm: 16, # Glob of Ectoplasm
+ ModelID.Monstrous_Eye: 20, # Monstrous Eye
+ ModelID.Monstrous_Fang: 21, # Monstrous Fang
+ ModelID.Feather: 11, # Feather
+ ModelID.Plant_Fiber: 10, # Plant Fiber
+ ModelID.Diamond: 24, # Diamond
+ ModelID.Onyx_Gemstone: 25, # Onyx Gemstone
+ ModelID.Ruby: 22, # Ruby
+ ModelID.Sapphire: 23, # Sapphire
+ ModelID.Tempered_Glass_Vial: 29, # Tempered Glass Vial
+ ModelID.Tanned_Hide_Square: 2, # Tanned Hide Square
+ ModelID.Fur_Square: 12, # Fur Square
+ ModelID.Leather_Square: 30, # Leather Square
+ ModelID.Elonian_Leather_Square: 31, # Elonian Leather Square
+ ModelID.Vial_Of_Ink: 32, # Vial of Ink
+ ModelID.Obsidian_Shard: 27, # Obsidian Shard
+ ModelID.Wood_Plank: 6, # Wood Plank
+ ModelID.Iron_Ingot: 1, # Iron Ingot
+ ModelID.Steel_Ingot: 17, # Steel Ingot
+ ModelID.Deldrimor_Steel_Ingot: 18, # Deldrimor Steel Ingot
+ ModelID.Roll_Of_Parchment: 33, # Roll of Parchment
+ ModelID.Roll_Of_Vellum: 34, # Roll of Vellum
+ ModelID.Scale: 3, # Scale
+ ModelID.Chitin_Fragment: 4, # Chitin Fragment
+ ModelID.Granite_Slab: 8, # Granite Slab
+ ModelID.Spiritwood_Plank: 35, # Spiritwood Plank
+ ModelID.Amber_Chunk: 36, # Amber Chunk
+ ModelID.Jadeite_Shard: 37, # Jadeite Shard
+}
+
+RARE_MATERIALS : list[int] = [
+ mat for mat, slot in MATERIAL_STORAGE_SLOTS.items() if slot > MATERIAL_STORAGE_SLOTS[ModelID.Feather]
+]
+
+COMMON_MATERIALS : list[int] = [
+ mat for mat, slot in MATERIAL_STORAGE_SLOTS.items() if slot <= MATERIAL_STORAGE_SLOTS[ModelID.Feather]
+]
diff --git a/Py4GWCoreLib/item_data/item_snapshot.py b/Py4GWCoreLib/item_data/item_snapshot.py
new file mode 100644
index 000000000..59b827e71
--- /dev/null
+++ b/Py4GWCoreLib/item_data/item_snapshot.py
@@ -0,0 +1,484 @@
+
+from typing import Optional, cast
+
+import PyInventory
+from PyItem import DyeInfo, ItemModifier, PyItem
+
+from Py4GWCoreLib.Item import Item
+from Py4GWCoreLib.enums_src.GameData_enums import Attribute, Profession, DyeColor
+from Py4GWCoreLib.enums_src.Item_enums import INVENTORY_BAGS, STORAGE_BAGS, Bags, ItemType, Rarity
+from Py4GWCoreLib.native_src.internals import string_table
+from Py4GWCoreLib.py4gwcorelib_src.FrameCache import frame_cache
+from Py4GWCoreLib.item_data.ItemData import ItemData
+from Py4GWCoreLib.item_mods_src.item_mod import ItemMod
+from Py4GWCoreLib.item_mods_src.item_modifier_parser import ItemModifierParser
+from Py4GWCoreLib.item_mods_src.properties import ArmorProperty, AttributeRequirement, DamageProperty, EnergyProperty, TargetItemTypeProperty
+from Py4GWCoreLib.item_mods_src.upgrades import Upgrade
+from Py4GWCoreLib.native_src.internals.encoded_strings import GWStringEncoded
+from Sources.frenkeyLib.ItemHandling.Items.ItemData import ITEM_DATA
+
+
+class _UnsetType:
+ __slots__ = ()
+
+
+_UNSET = _UnsetType()
+
+
+def get_item_bag(item_id: int, item_instance: Optional[PyItem] = None) -> Bags:
+ item = item_instance if item_instance and item_instance.item_id == item_id else Item.item_instance(item_id) if item_id > 0 else None
+
+ if not item or not item.IsItemValid(item_id):
+ return Bags.NoBag
+
+ bags_to_check : list[Bags] = []
+ if item.is_inventory_item:
+ bags_to_check.extend(INVENTORY_BAGS)
+
+ if item.is_storage_item:
+ bags_to_check.extend(STORAGE_BAGS)
+
+ for bag in bags_to_check:
+ inventory_bag = PyInventory.Bag(bag.value, bag.name)
+ if inventory_bag.FindItemById(item_id):
+ return bag
+
+ return Bags.NoBag
+
+
+class _LazyParsedItemData:
+ __slots__ = (
+ "modifiers",
+ "properties",
+ "prefix",
+ "suffix",
+ "inscription",
+ "inherent",
+ "attribute",
+ "requirement",
+ "min_damage",
+ "max_damage",
+ "target_item_type",
+ "energy",
+ "armor",
+ )
+
+ def __init__(
+ self,
+ modifiers: list[ItemModifier],
+ properties,
+ prefix: Optional[Upgrade],
+ suffix: Optional[Upgrade],
+ inscription: Optional[Upgrade],
+ inherent: Optional[list[Upgrade]],
+ attribute: Attribute,
+ requirement: int,
+ min_damage: int,
+ max_damage: int,
+ target_item_type: ItemType,
+ energy: int,
+ armor: int = 0,
+ ):
+ self.modifiers = modifiers
+ self.properties = properties
+ self.prefix = prefix
+ self.suffix = suffix
+ self.inscription = inscription
+ self.inherent = inherent
+ self.attribute = attribute
+ self.requirement = requirement
+ self.min_damage = min_damage
+ self.max_damage = max_damage
+ self.target_item_type = target_item_type
+ self.energy = energy
+ self.armor = armor
+
+
+class ItemSnapshot:
+ __slots__ = (
+ "id",
+ "is_valid",
+ "_name_enc",
+ "_singular_name",
+ "_complete_name_enc",
+ "_names",
+ "__bag",
+ "model_id",
+ "model_file_id",
+ "gw_dat_file_path",
+ "item_type",
+ "is_weapon",
+ "is_armor",
+ "rarity",
+ "profession",
+ "is_identified",
+ "value",
+ "is_usable",
+ "is_salvageable",
+ "is_salvage_kit",
+ "is_perfect_salvage_kit",
+ "is_inscribable",
+ "is_prefix_upgradable",
+ "is_suffix_upgradable",
+ "quantity",
+ "uses",
+ "_is_stackable",
+ "is_customized",
+ "dye_info",
+ "slot",
+ "is_inventory_item",
+ "is_storage_item",
+ "is_material",
+ "is_rare_material",
+ "is_material_salvageable",
+ "color",
+ "_parsed_item_data",
+ "_data",
+ )
+
+ def __init__(self, item_id: int, item_instance: Optional[PyItem] = None, bag: Optional[Bags] = None):
+ item = item_instance if item_instance and item_id == item_instance.item_id else Item.item_instance(item_id) if item_id > 0 else None
+
+ self.id: int = item_id
+ self.is_valid: bool = item.IsItemValid(item_id) if item else False
+
+ self._name_enc: Optional[bytes] | _UnsetType = _UNSET
+ self._singular_name: Optional[bytes] | _UnsetType = _UNSET
+ self._complete_name_enc: Optional[bytes] | _UnsetType = _UNSET
+ self._names: GWStringEncoded | _UnsetType = _UNSET
+
+ self.__bag: Optional[Bags] = bag if item and (item.is_inventory_item or item.is_storage_item) else Bags.NoBag
+
+ self.model_id: int = item.model_id if item else -1
+ self.model_file_id: int = item.model_file_id if item else -1
+ self.gw_dat_file_path = f"gwdat://{self.model_file_id}" if self.model_file_id > 0 else ""
+
+ self.item_type: ItemType = ItemType(item.item_type.ToInt()) if item else ItemType.Unknown
+ self.is_weapon = self.item_type.is_weapon_type() if item else False
+ self.is_armor = self.item_type.is_armor_type() if item else False
+
+ self.rarity: Rarity = Rarity(item.rarity.value) if item and item.rarity and item.rarity.value in Rarity._value2member_map_ else Rarity.White
+ self.profession: Profession = Profession(item.profession) if item and item.profession in Profession else Profession._None
+
+ self.is_identified: bool = item.is_identified if item else False
+ self.value: int = item.value if item else 0
+ self.is_usable: bool = item.is_usable if item else False
+ self.is_salvageable: bool = item.is_salvageable if item else False
+ self.is_salvage_kit: bool = item.is_salvage_kit if item else False
+ self.is_perfect_salvage_kit: bool = item.is_perfect_salvage_kit if item else False
+
+ self.is_inscribable: bool = item.is_inscribable if item else False
+ self.is_prefix_upgradable: bool = item.is_prefix_upgradable if item else False
+ self.is_suffix_upgradable: bool = item.is_suffix_upgradable if item else False
+
+ self.quantity: int = item.quantity if item else 0
+ self.uses: int = item.uses if item else 0
+ self._is_stackable: bool | _UnsetType = _UNSET
+ self.is_customized: bool = item.is_customized if item else False
+ self.dye_info: DyeInfo = item.dye_info if item else DyeInfo()
+
+ self.slot: int = item.slot if item else -1
+ self.is_inventory_item: bool = item.is_inventory_item if item else False
+ self.is_storage_item: bool = item.is_storage_item if item else False
+
+ self.is_material = item.is_material if item else False
+ self.is_rare_material = item.is_rare_material if item else False
+ self.is_material_salvageable = item.is_material_salvageable if item else False
+
+ self.color: DyeColor = DyeColor.from_dye_info(self.dye_info)
+
+ self._parsed_item_data: _LazyParsedItemData | _UnsetType = _UNSET
+ self._data: Optional[ItemData] | _UnsetType = _UNSET
+
+ @classmethod
+ @frame_cache(category="LazyItemSnapshot", source_lib="from_item_id")
+ def from_item_id(cls, item_id: int, item_instance: Optional[PyItem] = None, bag: Optional[Bags] = None) -> Optional['ItemSnapshot']:
+ item = item_instance if item_instance and item_id == item_instance.item_id else Item.item_instance(item_id) if item_id > 0 else None
+ is_valid = item.IsItemValid(item.item_id) if item else False
+
+ return cls(item.item_id, item, bag) if item and is_valid else None
+
+ @classmethod
+ def create(cls, item_id: int, item_instance: Optional[PyItem] = None, bag: Optional[Bags] = None) -> Optional['ItemSnapshot']:
+ item = item_instance if item_instance is not None else Item.item_instance(item_id) if item_id > 0 else None
+ is_valid = item.IsItemValid(item_id) if item else False
+
+ return cls(item_id, item, bag) if is_valid else None
+
+ def _load_name_bytes(self, loader) -> Optional[bytes]:
+ return bytes(loader(self.id)) if self.id > 0 and self.is_valid else None
+
+ def _get_parsed_item_data(self) -> _LazyParsedItemData:
+ if self._parsed_item_data is _UNSET:
+ modifiers: list[ItemModifier] = Item.Customization.Modifiers.GetModifiers(self.id) if self.id > 0 and self.is_valid else []
+ parser = ItemModifierParser(modifiers, self.rarity)
+ properties = parser.get_properties()
+
+ prefix, suffix, inscription, inherent = ItemMod.get_item_upgrades_from_properties(properties, self.rarity)
+
+ requirement = next((p for p in properties if isinstance(p, AttributeRequirement)), None)
+ damage = next((p for p in properties if isinstance(p, DamageProperty)), None)
+ target_item_type = next((p for p in properties if isinstance(p, TargetItemTypeProperty)), None)
+ energy = next((p for p in properties if isinstance(p, EnergyProperty)), None)
+ armor = next((p for p in properties if isinstance(p, ArmorProperty)), None) if self.is_armor else None
+
+ self._parsed_item_data = _LazyParsedItemData(
+ modifiers=modifiers,
+ properties=properties,
+ prefix=prefix,
+ suffix=suffix,
+ inscription=inscription,
+ inherent=inherent,
+ attribute=requirement.attribute if requirement else Attribute.None_,
+ requirement=requirement.attribute_level if requirement else 0,
+ min_damage=damage.min_damage if damage else 0,
+ max_damage=damage.max_damage if damage else 0,
+ target_item_type=target_item_type.item_type if target_item_type else ItemType.Unknown,
+ energy=energy.energy if energy else 0,
+ armor=armor.armor if armor else 0,
+ )
+
+ return cast(_LazyParsedItemData, self._parsed_item_data)
+
+ @property
+ def name_enc(self) -> Optional[bytes]:
+ if self._name_enc is _UNSET:
+ self._name_enc = self._load_name_bytes(PyItem.GetNameEnc)
+
+ return cast(Optional[bytes], self._name_enc)
+
+ @property
+ def singular_name_enc(self) -> Optional[bytes]:
+ if self._singular_name is _UNSET:
+ self._singular_name = self._load_name_bytes(PyItem.GetSingleItemName)
+
+ return cast(Optional[bytes], self._singular_name)
+
+ @property
+ def complete_name_enc(self) -> Optional[bytes]:
+ if self._complete_name_enc is _UNSET:
+ self._complete_name_enc = self._load_name_bytes(PyItem.GetCompleteNameEnc)
+
+ return cast(Optional[bytes], self._complete_name_enc)
+
+ @property
+ def names(self) -> GWStringEncoded:
+ if self._names is _UNSET:
+ self._names = GWStringEncoded(self.name_enc or bytes(), "Unknown Item")
+
+ return cast(GWStringEncoded, self._names)
+
+ @property
+ def info_string(self) -> str:
+ return "DISABLED"
+
+ @property
+ def name(self) -> str:
+ return string_table.decode(self.name_enc) if self.name_enc else ""
+
+ @property
+ def singular_name(self) -> str:
+ return string_table.decode(self.singular_name_enc) if self.singular_name_enc else ""
+
+ @property
+ def complete_name(self) -> str:
+ return string_table.decode(self.complete_name_enc) if self.complete_name_enc else ""
+
+ @property
+ def bag(self) -> Bags:
+ if self.__bag is None:
+ self.__bag = get_item_bag(self.id) if self.is_inventory_item or self.is_storage_item else Bags.NoBag
+
+ return self.__bag
+
+ @property
+ def is_stackable(self) -> bool:
+ if self._is_stackable is _UNSET:
+ self._is_stackable = Item.Customization.IsStackable(self.id) if self.id > 0 else False
+
+ return cast(bool, self._is_stackable)
+
+ @property
+ def modifiers(self) -> list[ItemModifier]:
+ return self._get_parsed_item_data().modifiers
+
+ @property
+ def properties(self):
+ return self._get_parsed_item_data().properties
+
+ @property
+ def has_upgrades(self) -> bool:
+ parsed_data = self._get_parsed_item_data()
+ return bool(parsed_data.prefix or parsed_data.suffix or parsed_data.inscription or (parsed_data.inherent and len(parsed_data.inherent) > 0))
+
+ @property
+ def prefix(self) -> Optional[Upgrade]:
+ return self._get_parsed_item_data().prefix
+
+ @property
+ def suffix(self) -> Optional[Upgrade]:
+ return self._get_parsed_item_data().suffix
+
+ @property
+ def inscription(self) -> Optional[Upgrade]:
+ return self._get_parsed_item_data().inscription
+
+ @property
+ def inherents(self) -> Optional[list[Upgrade]]:
+ return self._get_parsed_item_data().inherent
+
+ @property
+ def attribute(self) -> Attribute:
+ return self._get_parsed_item_data().attribute
+
+ @property
+ def requirement(self) -> int:
+ return self._get_parsed_item_data().requirement
+
+ @property
+ def min_damage(self) -> int:
+ return self._get_parsed_item_data().min_damage
+
+ @property
+ def energy(self) -> int:
+ return self._get_parsed_item_data().energy
+
+ @property
+ def armor(self) -> int:
+ return self._get_parsed_item_data().armor
+
+ @property
+ def max_damage(self) -> int:
+ return self._get_parsed_item_data().max_damage
+
+ @property
+ def target_item_type(self) -> ItemType:
+ return self._get_parsed_item_data().target_item_type
+
+ @property
+ def data(self) -> Optional[ItemData]:
+ # from Sources.frenkeyLib.DataCollector.collectors.items_collector import ITEMS
+
+ if self._data is _UNSET:
+ self._data = cast(Optional[ItemData], ITEM_DATA.get_item_data(model_id=self.model_id, item_type=self.item_type) if self.model_id != -1 else None)
+
+ return cast(Optional[ItemData], self._data)
+
+ def same_kind_as(self, other: 'ItemSnapshot') -> bool:
+ return self.model_id == other.model_id and self.item_type == other.item_type and (self.item_type != ItemType.Dye or self.color == other.color)
+
+ def update(self):
+ item = Item.item_instance(self.id) if self.id > 0 else None
+ self.is_valid = item.IsItemValid(self.id) if item else False
+
+ self._parsed_item_data = _UNSET
+ self._data = _UNSET
+ self._name_enc = _UNSET
+ self._singular_name = _UNSET
+ self._complete_name_enc = _UNSET
+ self._names = _UNSET
+ self._is_stackable = _UNSET
+
+ if not self.is_valid or not item:
+ self.__bag = Bags.NoBag
+ return
+
+ self.model_id = item.model_id
+ self.model_file_id = item.model_file_id
+ self.gw_dat_file_path = f"gwdat://{self.model_file_id}" if self.model_file_id > 0 else ""
+
+ self.item_type = ItemType(item.item_type.ToInt())
+ self.rarity = Rarity(item.rarity.value) if item.rarity and item.rarity.value in Rarity._value2member_map_ else Rarity.White
+ self.profession = Profession(item.profession) if item.profession in Profession else Profession._None
+
+ self.is_identified = item.is_identified
+ self.value = item.value
+ self.is_usable = item.is_usable
+ self.is_salvageable = item.is_salvageable
+ self.is_salvage_kit = item.is_salvage_kit
+ self.is_perfect_salvage_kit = item.is_perfect_salvage_kit
+ self.is_inscribable = item.is_inscribable
+ self.is_prefix_upgradable = item.is_prefix_upgradable
+ self.is_suffix_upgradable = item.is_suffix_upgradable
+ self.quantity = item.quantity
+ self.uses = item.uses
+ self.is_customized = item.is_customized
+ self.dye_info = item.dye_info
+ self.slot = item.slot
+ self.is_inventory_item = item.is_inventory_item
+ self.is_storage_item = item.is_storage_item
+ self.is_material = item.is_material
+ self.is_rare_material = item.is_rare_material
+ self.is_material_salvageable = item.is_material_salvageable
+ self.color = DyeColor.from_dye_info(self.dye_info)
+ self.__bag = None if item.is_inventory_item or item.is_storage_item else Bags.NoBag
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_bag_snapshot")
+ def get_bag_snapshot(bag: Bags) -> dict[int, Optional['ItemSnapshot']]:
+ inventory_bag = PyInventory.Bag(bag.value, bag.name)
+ bag_snapshot: dict[int, Optional[ItemSnapshot]] = {}
+
+ bag_size = inventory_bag.GetSize()
+ if bag_size <= 0:
+ return bag_snapshot
+
+ for slot in range(bag_size):
+ bag_snapshot[slot] = None
+
+ for item in inventory_bag.GetItems():
+ slot = item.slot # real slot of the item
+ bag_snapshot[slot] = ItemSnapshot.from_item_id(item.item_id, item, bag) if item else None
+
+
+ return bag_snapshot
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_inventory_snapshot")
+ def get_inventory_snapshot(start_bag: Bags = Bags.Backpack, end_bag: Bags = Bags.Bag2) -> dict[Bags, dict[int, Optional['ItemSnapshot']]]:
+ bags = [Bags(bag_id) for bag_id in range(start_bag.value, end_bag.value + 1)]
+ return ItemSnapshot.get_bags_snapshot(bags)
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_bags_snapshot")
+ def get_bags_snapshot(bags: list[Bags]) -> dict[Bags, dict[int, Optional['ItemSnapshot']]]:
+ snapshot = {}
+
+ for bag in bags:
+ bag_snapshot = ItemSnapshot.get_bag_snapshot(bag)
+
+ if len(bag_snapshot) > 0:
+ snapshot[bag] = bag_snapshot
+
+ return snapshot
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_items")
+ def get_bags_items(bags: list[Bags]) -> list['ItemSnapshot']:
+ snapshot = ItemSnapshot.get_bags_snapshot(bags)
+ items: list[ItemSnapshot] = []
+
+ for bag in snapshot.values():
+ for item in bag.values():
+ if item is not None and item.is_valid:
+ items.append(item)
+
+ return items
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_items")
+ def get_bag_items(bag: Bags) -> list['ItemSnapshot']:
+ snapshot = ItemSnapshot.get_bag_snapshot(bag)
+ return [item for item in snapshot.values() if item is not None and item.is_valid]
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_items")
+ def get_items(item_ids : list[int]) -> list['ItemSnapshot']:
+ return [item for item in (ItemSnapshot.from_item_id(item_id) for item_id in item_ids) if item is not None and item.is_valid]
+
+ @staticmethod
+ @frame_cache(category="ItemSnapshot", source_lib="get_item_count")
+ def get_item_count(item : "ItemSnapshot", bags: list[Bags] = [*INVENTORY_BAGS, *STORAGE_BAGS, Bags.MaterialStorage]) -> int:
+ items = ItemSnapshot.get_bags_items(bags)
+
+ return sum(i.quantity for i in items if i.is_valid and i.same_kind_as(item))
\ No newline at end of file
diff --git a/Py4GWCoreLib/item_mods_src/item_modifier_parser.py b/Py4GWCoreLib/item_mods_src/item_modifier_parser.py
index 27f7cb179..a6f08055e 100644
--- a/Py4GWCoreLib/item_mods_src/item_modifier_parser.py
+++ b/Py4GWCoreLib/item_mods_src/item_modifier_parser.py
@@ -24,12 +24,12 @@ def _decode(self, runtime_modifiers: list[ItemModifier]):
def _build_properties(self):
from Py4GWCoreLib.item_mods_src.upgrades import _INHERENT_UPGRADES
- from Py4GWCoreLib.item_mods_src.upgrade_parser import get_property_factory
+ from Py4GWCoreLib.item_mods_src.upgrade_parser import UpgradeParser
handled_modifiers : list[DecodedModifier] = []
try:
for mod in self.modifiers:
- factory = get_property_factory().get(mod.identifier)
+ factory = UpgradeParser.get_property_factory().get(mod.identifier)
if factory:
prop = factory(mod, self.modifiers, self.rarity)
diff --git a/Py4GWCoreLib/item_mods_src/upgrade_parser.py b/Py4GWCoreLib/item_mods_src/upgrade_parser.py
index a3b96dce9..86b1c8e92 100644
--- a/Py4GWCoreLib/item_mods_src/upgrade_parser.py
+++ b/Py4GWCoreLib/item_mods_src/upgrade_parser.py
@@ -9,169 +9,177 @@
from Py4GWCoreLib.item_mods_src.types import ItemBaneSpecies, ItemUpgradeType, ModifierIdentifier
from Py4GWCoreLib.item_mods_src.upgrades import Upgrade, UnknownUpgrade, _UPGRADES
-def get_profession_from_attribute(attribute: Attribute) -> Optional[Profession]:
- return _ATTRIBUTE_TO_PROFESSION.get(attribute, Profession._None)
+class UpgradeParser:
+ @staticmethod
+ def get_profession_from_attribute(attribute: Attribute) -> Optional[Profession]:
+ return _ATTRIBUTE_TO_PROFESSION.get(attribute, Profession._None)
-def get_species(modifiers: list[DecodedModifier]) -> ItemBaneSpecies:
- bane_mod = next((m for m in modifiers if m.identifier == ModifierIdentifier.BaneSpecies), None)
- if bane_mod:
- return ItemBaneSpecies(bane_mod.arg1)
-
- return ItemBaneSpecies.Unknown
+ @staticmethod
+ def get_species(modifiers: list[DecodedModifier]) -> ItemBaneSpecies:
+ bane_mod = next((m for m in modifiers if m.identifier == ModifierIdentifier.BaneSpecies), None)
+ if bane_mod:
+ return ItemBaneSpecies(bane_mod.arg1)
+
+ return ItemBaneSpecies.Unknown
-def get_upgrade_property(modifier: DecodedModifier, all_modifiers: list[DecodedModifier], upgrade_type: ItemUpgradeType | None = None, rarity: Rarity = Rarity.Blue) -> Optional[ItemProperty]:
-
- # all modifiers coming AFTER the current modifier
- remaining_modifiers = all_modifiers[all_modifiers.index(modifier) + 1:]
- upgrade, upgrade_type = get_upgrade(modifier, remaining_modifiers, all_modifiers, upgrade_type, rarity)
+ @staticmethod
+ def get_upgrade_property(modifier: DecodedModifier, all_modifiers: list[DecodedModifier], upgrade_type: ItemUpgradeType | None = None, rarity: Rarity = Rarity.Blue) -> Optional[ItemProperty]:
- if upgrade and upgrade.mod_type != ItemUpgradeType.Unknown:
- match upgrade_type:
- case ItemUpgradeType.Prefix:
- return PrefixProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
-
- case ItemUpgradeType.Suffix:
- return SuffixProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
-
- case ItemUpgradeType.Inscription:
- return InscriptionProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
-
- case ItemUpgradeType.UpgradeRune:
- return UpgradeRuneProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
+ # all modifiers coming AFTER the current modifier
+ remaining_modifiers = all_modifiers[all_modifiers.index(modifier) + 1:]
+ upgrade, upgrade_type = UpgradeParser.get_upgrade(modifier, remaining_modifiers, all_modifiers, upgrade_type, rarity)
- case ItemUpgradeType.AppliesToRune:
- return AppliesToRuneProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
-
- return None
+ if upgrade and upgrade.mod_type != ItemUpgradeType.Unknown:
+ match upgrade_type:
+ case ItemUpgradeType.Prefix:
+ return PrefixProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
+
+ case ItemUpgradeType.Suffix:
+ return SuffixProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
+
+ case ItemUpgradeType.Inscription:
+ return InscriptionProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
+
+ case ItemUpgradeType.UpgradeRune:
+ return UpgradeRuneProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
+
+ case ItemUpgradeType.AppliesToRune:
+ return AppliesToRuneProperty(modifier=modifier, upgrade_id=modifier.upgrade_id, upgrade=upgrade, rarity=rarity)
+
+ return None
-def get_upgrade(modifier : DecodedModifier, remaining_modifiers: list[DecodedModifier], all_modifiers: list[DecodedModifier], upgrade_type: ItemUpgradeType | None = None, rarity: Rarity = Rarity.Blue) -> tuple["Upgrade", ItemUpgradeType]:
- creator_types = [t for t in _UPGRADES if t.has_id(modifier.upgrade_id) and (upgrade_type is None or t.mod_type == upgrade_type)]
- matches: list[tuple[type[Upgrade], Upgrade]] = []
+ @staticmethod
+ def get_upgrade(modifier : DecodedModifier, remaining_modifiers: list[DecodedModifier], all_modifiers: list[DecodedModifier], upgrade_type: ItemUpgradeType | None = None, rarity: Rarity = Rarity.Blue) -> tuple["Upgrade", ItemUpgradeType]:
+ creator_types = [t for t in _UPGRADES if t.has_id(modifier.upgrade_id) and (upgrade_type is None or t.mod_type == upgrade_type)]
+ matches: list[tuple[type[Upgrade], Upgrade]] = []
- for creator_type in creator_types:
- if creator_type is not None:
- upgrade = creator_type.compose_from_modifiers(modifier, remaining_modifiers.copy(), all_modifiers, rarity)
- if upgrade is not None:
- matches.append((creator_type, upgrade))
+ for creator_type in creator_types:
+ if creator_type is not None:
+ upgrade = creator_type.compose_from_modifiers(modifier, remaining_modifiers.copy(), all_modifiers, rarity)
+ if upgrade is not None:
+ matches.append((creator_type, upgrade))
- if matches:
- best_creator_type, best_upgrade = max(
- matches,
- key=lambda entry: entry[0].get_specificity_score(),
- )
- return best_upgrade, best_creator_type.mod_type
-
- return UnknownUpgrade(), ItemUpgradeType.Unknown
+ if matches:
+ best_creator_type, best_upgrade = max(
+ matches,
+ key=lambda entry: entry[0].get_specificity_score(),
+ )
+ return best_upgrade, best_creator_type.mod_type
+
+ return UnknownUpgrade(), ItemUpgradeType.Unknown
-def get_damage_type(modifiers: list[DecodedModifier]) -> DamageType:
- damage_type_mod = next((m for m in modifiers if m.identifier == ModifierIdentifier.DamageTypeProperty), None)
-
- if damage_type_mod:
- return DamageType(damage_type_mod.arg1)
-
- return DamageType.Unknown
+ @staticmethod
+ def get_damage_type(modifiers: list[DecodedModifier]) -> DamageType:
+ damage_type_mod = next((m for m in modifiers if m.identifier == ModifierIdentifier.DamageTypeProperty), None)
+
+ if damage_type_mod:
+ return DamageType(damage_type_mod.arg1)
+
+ return DamageType.Unknown
-def get_item_requirement(modifiers: list[DecodedModifier]) -> Attribute:
- item_req_mod = next((m for m in modifiers if m.identifier == ModifierIdentifier.AttributeRequirement), None)
-
- if item_req_mod:
- return Attribute(item_req_mod.arg1)
-
- return Attribute.None_
+ @staticmethod
+ def get_item_requirement(modifiers: list[DecodedModifier]) -> Attribute:
+ item_req_mod = next((m for m in modifiers if m.identifier == ModifierIdentifier.AttributeRequirement), None)
+
+ if item_req_mod:
+ return Attribute(item_req_mod.arg1)
+
+ return Attribute.None_
+ @staticmethod
+ def get_property_factory() -> dict[ModifierIdentifier, Callable[[DecodedModifier, list[DecodedModifier], Rarity], ItemProperty]]:
+ return {
+ ModifierIdentifier.Empty: lambda m, _, rarity: EmptyProperty(modifier=m, rarity=rarity),
+ ModifierIdentifier.Armor1: lambda m, _, rarity: ArmorProperty(modifier=m, armor=m.arg1, rarity=rarity),
+ ModifierIdentifier.Armor2: lambda m, _, rarity: ArmorProperty(modifier=m, armor=m.arg1, rarity=rarity),
+ ModifierIdentifier.ArmorMinusAttacking: lambda m, _, rarity: ArmorMinusAttacking(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPenetration: lambda m, _, rarity: ArmorPenetration(modifier=m, armor_penetration=m.arg2, chance=m.arg1, rarity=rarity),
+ ModifierIdentifier.ArmorPlus: lambda m, _, rarity: ArmorPlus(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusAttacking: lambda m, _, rarity: ArmorPlusAttacking(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusCasting: lambda m, _, rarity: ArmorPlusCasting(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusEnchanted: lambda m, _, rarity: ArmorPlusEnchanted(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusHexed: lambda m, _, rarity: ArmorPlusHexed(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusAbove: lambda m, _, rarity: ArmorPlusAbove(modifier=m, armor=m.arg2, health_threshold=m.arg1, rarity=rarity),
+ ModifierIdentifier.ArmorPlusVsDamage: lambda m, _, rarity: ArmorPlusVsDamage(modifier=m, armor=m.arg2, damage_type=DamageType(m.arg1), rarity=rarity),
+ ModifierIdentifier.ArmorPlusVsElemental: lambda m, _, rarity: ArmorPlusVsElemental(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusVsPhysical: lambda m, _, rarity: ArmorPlusVsPhysical(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusVsPhysical2: lambda m, _, rarity: ArmorPlusVsPhysical(modifier=m, armor=m.arg2, rarity=rarity),
+ ModifierIdentifier.ArmorPlusVsSpecies: lambda m, _, rarity: ArmorPlusVsSpecies(modifier=m, armor=m.arg2, species=ItemBaneSpecies(m.arg1), rarity=rarity),
+ ModifierIdentifier.ArmorPlusWhileBelow: lambda m, _, rarity: ArmorPlusWhileBelow(modifier=m, armor=m.arg2, health_threshold=m.arg1, rarity=rarity),
+ ModifierIdentifier.AttributePlusOne: lambda m, _, rarity: AttributePlusOne(modifier=m, attribute=Attribute(m.arg1), chance=m.arg2, rarity=rarity),
+ ModifierIdentifier.AttributePlusOneItem: lambda m, _, rarity: AttributePlusOneItem(modifier=m, chance=m.arg1, rarity=rarity),
+ ModifierIdentifier.AttributeRequirement: lambda m, _, rarity: AttributeRequirement(modifier=m, attribute=Attribute(m.arg1), attribute_level=m.arg2, rarity=rarity),
+ ModifierIdentifier.BaneSpecies: lambda m, _, rarity: BaneProperty(modifier=m, species=ItemBaneSpecies(m.arg1), rarity=rarity),
+ ModifierIdentifier.Damage: lambda m, mods, rarity: DamageProperty(modifier=m, min_damage=m.arg2, max_damage=m.arg1, damage_type=UpgradeParser.get_damage_type(mods), rarity=rarity),
+ ModifierIdentifier.Damage2: lambda m, mods, rarity: DamageProperty(modifier=m, min_damage=m.arg2, max_damage=m.arg1, damage_type=UpgradeParser.get_damage_type(mods), rarity=rarity),
+ ModifierIdentifier.DamageCustomized: lambda m, _, rarity: DamageCustomized(modifier=m, damage_increase=m.arg1 - 100, rarity=rarity),
+ ModifierIdentifier.DamagePlusEnchanted: lambda m, _, rarity: DamagePlusEnchanted(modifier=m, damage_increase=m.arg2, rarity=rarity),
+ ModifierIdentifier.DamagePlusHexed: lambda m, _, rarity: DamagePlusHexed(modifier=m, damage_increase=m.arg2, rarity=rarity),
+ ModifierIdentifier.DamagePlusPercent: lambda m, _, rarity: DamagePlusPercent(modifier=m, damage_increase=m.arg2, rarity=rarity),
+ ModifierIdentifier.DamagePlusStance: lambda m, _, rarity: DamagePlusStance(modifier=m, damage_increase=m.arg2, rarity=rarity),
+ ModifierIdentifier.DamagePlusVsHexed: lambda m, _, rarity: DamagePlusVsHexed(modifier=m, damage_increase=m.arg2, rarity=rarity),
+ ModifierIdentifier.DamagePlusVsSpecies: lambda m, mods, rarity: DamagePlusVsSpecies(modifier=m, damage_increase=m.arg1, species=UpgradeParser.get_species(mods), rarity=rarity),
+ ModifierIdentifier.DamagePlusWhileBelow: lambda m, _, rarity: DamagePlusWhileBelow(modifier=m, damage_increase=m.arg2, health_threshold=m.arg1, rarity=rarity),
+ ModifierIdentifier.DamagePlusWhileAbove: lambda m, _, rarity: DamagePlusWhileAbove(modifier=m, damage_increase=m.arg2, health_threshold=m.arg1, rarity=rarity),
+ ModifierIdentifier.DamageTypeProperty: lambda m, _, rarity: DamageTypeProperty(modifier=m, damage_type=DamageType(m.arg1), rarity=rarity),
+ ModifierIdentifier.Energy: lambda m, _, rarity: EnergyProperty(modifier=m, energy=m.arg1, rarity=rarity),
+ ModifierIdentifier.Energy2: lambda m, _, rarity: EnergyProperty(modifier=m, energy=m.arg1, rarity=rarity),
+ ModifierIdentifier.EnergyRecovery: lambda m, _, rarity: EnergyRecovery(modifier=m, energy_regen=m.arg1, rarity=rarity),
+ ModifierIdentifier.EnergyRegeneration: lambda m, _, rarity: EnergyRegeneration(modifier=m, energy_regeneration=-m.arg2, rarity=rarity),
+ ModifierIdentifier.EnergyGainOnHit: lambda m, _, rarity: EnergyGainOnHit(modifier=m, energy_gain=m.arg2, rarity=rarity),
+ ModifierIdentifier.EnergyMinus: lambda m, _, rarity: EnergyMinus(modifier=m, energy=m.arg2, rarity=rarity),
+ ModifierIdentifier.EnergyPlus : lambda m, _, rarity: EnergyPlus(modifier=m, energy=m.arg2, rarity=rarity),
+ ModifierIdentifier.EnergyPlusEnchanted: lambda m, _, rarity: EnergyPlusEnchanted(modifier=m, energy=m.arg2, rarity=rarity),
+ ModifierIdentifier.EnergyPlusHexed: lambda m, _, rarity: EnergyPlusHexed(modifier=m, energy=m.arg2, rarity=rarity),
+ ModifierIdentifier.EnergyPlusWhileBelow: lambda m, _, rarity: EnergyPlusWhileBelow(modifier=m, energy=m.arg2, health_threshold=m.arg1, rarity=rarity),
+ ModifierIdentifier.EnergyPlusWhileAbove: lambda m, _, rarity: EnergyPlusWhileAbove(modifier=m, energy=m.arg2, health_threshold=m.arg1, rarity=rarity),
+ ModifierIdentifier.Furious: lambda m, _, rarity: Furious(modifier=m, chance=m.arg2, rarity=rarity),
+ ModifierIdentifier.HalvesCastingTimeAttribute: lambda m, _, rarity: HalvesCastingTimeAttribute(modifier=m, chance=m.arg1, attribute=Attribute(m.arg2), rarity=rarity),
+ ModifierIdentifier.HalvesCastingTimeGeneral: lambda m, _, rarity: HalvesCastingTimeGeneral(modifier=m, chance=m.arg1, rarity=rarity),
+ ModifierIdentifier.HalvesCastingTimeItemAttribute: lambda m, mods, rarity: HalvesCastingTimeItemAttribute(modifier=m, chance=m.arg1, attribute=UpgradeParser.get_item_requirement(mods), rarity=rarity),
+ ModifierIdentifier.HalvesSkillRechargeAttribute: lambda m, _, rarity: HalvesSkillRechargeAttribute(modifier=m, chance=m.arg1, attribute=Attribute(m.arg2), rarity=rarity),
+ ModifierIdentifier.HalvesSkillRechargeGeneral: lambda m, _, rarity: HalvesSkillRechargeGeneral(modifier=m, chance=m.arg1, rarity=rarity),
+ ModifierIdentifier.HalvesSkillRechargeItemAttribute: lambda m, mods, rarity: HalvesSkillRechargeItemAttribute(modifier=m, chance=m.arg1, attribute=UpgradeParser.get_item_requirement(mods), rarity=rarity),
+ ModifierIdentifier.HeadpieceAttribute: lambda m, _, rarity: HeadpieceAttribute(modifier=m, attribute=Attribute(m.arg1), attribute_level=m.arg2, rarity=rarity),
+ ModifierIdentifier.HeadpieceGenericAttribute: lambda m, _, rarity: HeadpieceGenericAttribute(modifier=m, rarity=rarity),
+ ModifierIdentifier.HealthRegeneneration: lambda m, _, rarity: HealthRegeneneration(modifier=m, health_regeneration=-m.arg2, rarity=rarity),
+ ModifierIdentifier.HealthMinus: lambda m, _, rarity: HealthMinus(modifier=m, health_reduction=m.arg2, rarity=rarity),
+ ModifierIdentifier.HealthPlus: lambda m, _, rarity: HealthPlus(modifier=m, health=m.arg1, rarity=rarity),
+ ModifierIdentifier.HealthPlus2 : lambda m, _, rarity: HealthPlus(modifier=m, health=m.arg2, rarity=rarity),
+ ModifierIdentifier.HealthPlusEnchanted: lambda m, _, rarity: HealthPlusEnchanted(modifier=m, health=m.arg1, rarity=rarity),
+ ModifierIdentifier.HealthPlusHexed: lambda m, _, rarity: HealthPlusHexed(modifier=m, health=m.arg1, rarity=rarity),
+ ModifierIdentifier.HealthPlusStance: lambda m, _, rarity: HealthPlusStance(modifier=m, health=m.arg1, rarity=rarity),
+ ModifierIdentifier.HealthStealOnHit: lambda m, _, rarity: HealthStealOnHit(modifier=m, health_steal=m.arg1, rarity=rarity),
+ ModifierIdentifier.HighlySalvageable: lambda m, _, rarity: HighlySalvageable(modifier=m, rarity=rarity),
+ ModifierIdentifier.IncreaseConditionDuration: lambda m, _, rarity: IncreaseConditionDuration(modifier=m, condition=Ailment(m.arg2), rarity=rarity),
+ ModifierIdentifier.IncreaseEnchantmentDuration: lambda m, _, rarity: IncreaseEnchantmentDuration(modifier=m, enchantment_duration=m.arg2, rarity=rarity),
+ ModifierIdentifier.IncreasedSaleValue: lambda m, _, rarity: IncreasedSaleValue(modifier=m, rarity=rarity),
+ ModifierIdentifier.Infused: lambda m, _, rarity: Infused(modifier=m, rarity=rarity),
+ ModifierIdentifier.OfTheProfession: lambda m, _, rarity: OfTheProfession(modifier=m, attribute=Attribute(m.arg1), attribute_level=m.arg2, profession=UpgradeParser.get_profession_from_attribute(Attribute(m.arg1)) or Profession._None, rarity=rarity),
+ ModifierIdentifier.ReceiveLessPhysDamageEnchanted: lambda m, _, rarity: ReceiveLessPhysDamageEnchanted(modifier=m, damage_reduction=m.arg2, rarity=rarity),
+ ModifierIdentifier.ReceiveLessPhysDamageHexed: lambda m, _, rarity: ReceiveLessPhysDamageHexed(modifier=m, damage_reduction=m.arg2, rarity=rarity),
+ ModifierIdentifier.ReceiveLessPhysDamageStance: lambda m, _, rarity: ReceiveLessPhysDamageStance(modifier=m, damage_reduction=m.arg2, rarity=rarity),
+ ModifierIdentifier.ReduceConditionDuration: lambda m, _, rarity: ReduceConditionDuration(modifier=m, condition=Reduced_Ailment(m.arg1), rarity=rarity),
+ ModifierIdentifier.ReduceConditionTupleDuration: lambda m, _, rarity: ReduceConditionTupleDuration(modifier=m, condition_1=Reduced_Ailment(m.arg2), condition_2=Reduced_Ailment(m.arg1), rarity=rarity),
+ ModifierIdentifier.ReducesDiseaseDuration: lambda m, _, rarity: ReducesDiseaseDuration(modifier=m, rarity=rarity),
+ ModifierIdentifier.ReceiveLessDamage: lambda m, _, rarity: ReceiveLessDamage(modifier=m, damage_reduction=m.arg2, chance=m.arg1, rarity=rarity),
+ ModifierIdentifier.TargetItemType: lambda m, _, rarity: TargetItemTypeProperty(modifier=m, item_type=ItemType(m.arg1), rarity=rarity),
+ ModifierIdentifier.TooltipDescription: lambda m, _, rarity: TooltipProperty(modifier=m, rarity=rarity),
+
+ ModifierIdentifier.AttributeRune: lambda m, mods, rarity:
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.Prefix, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.Inscription, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.Suffix, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.UpgradeRune, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.AppliesToRune, rarity) or
+ UnknownUpgradeProperty(modifier=m, upgrade_id=m.upgrade_id, rarity=rarity),
+
+ ModifierIdentifier.Upgrade: lambda m, mods, rarity:
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.Prefix, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.Inscription, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.Suffix, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.UpgradeRune, rarity) or
+ UpgradeParser.get_upgrade_property(m, mods, ItemUpgradeType.AppliesToRune, rarity) or
+ UnknownUpgradeProperty(modifier=m, upgrade_id=m.upgrade_id, rarity=rarity),
+ }
-def get_property_factory() -> dict[ModifierIdentifier, Callable[[DecodedModifier, list[DecodedModifier], Rarity], ItemProperty]]:
- return {
- ModifierIdentifier.Empty: lambda m, _, rarity: EmptyProperty(modifier=m, rarity=rarity),
- ModifierIdentifier.Armor1: lambda m, _, rarity: ArmorProperty(modifier=m, armor=m.arg1, rarity=rarity),
- ModifierIdentifier.Armor2: lambda m, _, rarity: ArmorProperty(modifier=m, armor=m.arg1, rarity=rarity),
- ModifierIdentifier.ArmorMinusAttacking: lambda m, _, rarity: ArmorMinusAttacking(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPenetration: lambda m, _, rarity: ArmorPenetration(modifier=m, armor_penetration=m.arg2, chance=m.arg1, rarity=rarity),
- ModifierIdentifier.ArmorPlus: lambda m, _, rarity: ArmorPlus(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusAttacking: lambda m, _, rarity: ArmorPlusAttacking(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusCasting: lambda m, _, rarity: ArmorPlusCasting(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusEnchanted: lambda m, _, rarity: ArmorPlusEnchanted(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusHexed: lambda m, _, rarity: ArmorPlusHexed(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusAbove: lambda m, _, rarity: ArmorPlusAbove(modifier=m, armor=m.arg2, health_threshold=m.arg1, rarity=rarity),
- ModifierIdentifier.ArmorPlusVsDamage: lambda m, _, rarity: ArmorPlusVsDamage(modifier=m, armor=m.arg2, damage_type=DamageType(m.arg1), rarity=rarity),
- ModifierIdentifier.ArmorPlusVsElemental: lambda m, _, rarity: ArmorPlusVsElemental(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusVsPhysical: lambda m, _, rarity: ArmorPlusVsPhysical(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusVsPhysical2: lambda m, _, rarity: ArmorPlusVsPhysical(modifier=m, armor=m.arg2, rarity=rarity),
- ModifierIdentifier.ArmorPlusVsSpecies: lambda m, _, rarity: ArmorPlusVsSpecies(modifier=m, armor=m.arg2, species=ItemBaneSpecies(m.arg1), rarity=rarity),
- ModifierIdentifier.ArmorPlusWhileBelow: lambda m, _, rarity: ArmorPlusWhileBelow(modifier=m, armor=m.arg2, health_threshold=m.arg1, rarity=rarity),
- ModifierIdentifier.AttributePlusOne: lambda m, _, rarity: AttributePlusOne(modifier=m, attribute=Attribute(m.arg1), chance=m.arg2, rarity=rarity),
- ModifierIdentifier.AttributePlusOneItem: lambda m, _, rarity: AttributePlusOneItem(modifier=m, chance=m.arg1, rarity=rarity),
- ModifierIdentifier.AttributeRequirement: lambda m, _, rarity: AttributeRequirement(modifier=m, attribute=Attribute(m.arg1), attribute_level=m.arg2, rarity=rarity),
- ModifierIdentifier.BaneSpecies: lambda m, _, rarity: BaneProperty(modifier=m, species=ItemBaneSpecies(m.arg1), rarity=rarity),
- ModifierIdentifier.Damage: lambda m, mods, rarity: DamageProperty(modifier=m, min_damage=m.arg2, max_damage=m.arg1, damage_type=get_damage_type(mods), rarity=rarity),
- ModifierIdentifier.Damage2: lambda m, mods, rarity: DamageProperty(modifier=m, min_damage=m.arg2, max_damage=m.arg1, damage_type=get_damage_type(mods), rarity=rarity),
- ModifierIdentifier.DamageCustomized: lambda m, _, rarity: DamageCustomized(modifier=m, damage_increase=m.arg1 - 100, rarity=rarity),
- ModifierIdentifier.DamagePlusEnchanted: lambda m, _, rarity: DamagePlusEnchanted(modifier=m, damage_increase=m.arg2, rarity=rarity),
- ModifierIdentifier.DamagePlusHexed: lambda m, _, rarity: DamagePlusHexed(modifier=m, damage_increase=m.arg2, rarity=rarity),
- ModifierIdentifier.DamagePlusPercent: lambda m, _, rarity: DamagePlusPercent(modifier=m, damage_increase=m.arg2, rarity=rarity),
- ModifierIdentifier.DamagePlusStance: lambda m, _, rarity: DamagePlusStance(modifier=m, damage_increase=m.arg2, rarity=rarity),
- ModifierIdentifier.DamagePlusVsHexed: lambda m, _, rarity: DamagePlusVsHexed(modifier=m, damage_increase=m.arg2, rarity=rarity),
- ModifierIdentifier.DamagePlusVsSpecies: lambda m, mods, rarity: DamagePlusVsSpecies(modifier=m, damage_increase=m.arg1, species=get_species(mods), rarity=rarity),
- ModifierIdentifier.DamagePlusWhileBelow: lambda m, _, rarity: DamagePlusWhileBelow(modifier=m, damage_increase=m.arg2, health_threshold=m.arg1, rarity=rarity),
- ModifierIdentifier.DamagePlusWhileAbove: lambda m, _, rarity: DamagePlusWhileAbove(modifier=m, damage_increase=m.arg2, health_threshold=m.arg1, rarity=rarity),
- ModifierIdentifier.DamageTypeProperty: lambda m, _, rarity: DamageTypeProperty(modifier=m, damage_type=DamageType(m.arg1), rarity=rarity),
- ModifierIdentifier.Energy: lambda m, _, rarity: EnergyProperty(modifier=m, energy=m.arg1, rarity=rarity),
- ModifierIdentifier.Energy2: lambda m, _, rarity: EnergyProperty(modifier=m, energy=m.arg1, rarity=rarity),
- ModifierIdentifier.EnergyRecovery: lambda m, _, rarity: EnergyRecovery(modifier=m, energy_regen=m.arg1, rarity=rarity),
- ModifierIdentifier.EnergyRegeneration: lambda m, _, rarity: EnergyRegeneration(modifier=m, energy_regeneration=-m.arg2, rarity=rarity),
- ModifierIdentifier.EnergyGainOnHit: lambda m, _, rarity: EnergyGainOnHit(modifier=m, energy_gain=m.arg2, rarity=rarity),
- ModifierIdentifier.EnergyMinus: lambda m, _, rarity: EnergyMinus(modifier=m, energy=m.arg2, rarity=rarity),
- ModifierIdentifier.EnergyPlus : lambda m, _, rarity: EnergyPlus(modifier=m, energy=m.arg2, rarity=rarity),
- ModifierIdentifier.EnergyPlusEnchanted: lambda m, _, rarity: EnergyPlusEnchanted(modifier=m, energy=m.arg2, rarity=rarity),
- ModifierIdentifier.EnergyPlusHexed: lambda m, _, rarity: EnergyPlusHexed(modifier=m, energy=m.arg2, rarity=rarity),
- ModifierIdentifier.EnergyPlusWhileBelow: lambda m, _, rarity: EnergyPlusWhileBelow(modifier=m, energy=m.arg2, health_threshold=m.arg1, rarity=rarity),
- ModifierIdentifier.EnergyPlusWhileAbove: lambda m, _, rarity: EnergyPlusWhileAbove(modifier=m, energy=m.arg2, health_threshold=m.arg1, rarity=rarity),
- ModifierIdentifier.Furious: lambda m, _, rarity: Furious(modifier=m, chance=m.arg2, rarity=rarity),
- ModifierIdentifier.HalvesCastingTimeAttribute: lambda m, _, rarity: HalvesCastingTimeAttribute(modifier=m, chance=m.arg1, attribute=Attribute(m.arg2), rarity=rarity),
- ModifierIdentifier.HalvesCastingTimeGeneral: lambda m, _, rarity: HalvesCastingTimeGeneral(modifier=m, chance=m.arg1, rarity=rarity),
- ModifierIdentifier.HalvesCastingTimeItemAttribute: lambda m, mods, rarity: HalvesCastingTimeItemAttribute(modifier=m, chance=m.arg1, attribute=get_item_requirement(mods), rarity=rarity),
- ModifierIdentifier.HalvesSkillRechargeAttribute: lambda m, _, rarity: HalvesSkillRechargeAttribute(modifier=m, chance=m.arg1, attribute=Attribute(m.arg2), rarity=rarity),
- ModifierIdentifier.HalvesSkillRechargeGeneral: lambda m, _, rarity: HalvesSkillRechargeGeneral(modifier=m, chance=m.arg1, rarity=rarity),
- ModifierIdentifier.HalvesSkillRechargeItemAttribute: lambda m, mods, rarity: HalvesSkillRechargeItemAttribute(modifier=m, chance=m.arg1, attribute=get_item_requirement(mods), rarity=rarity),
- ModifierIdentifier.HeadpieceAttribute: lambda m, _, rarity: HeadpieceAttribute(modifier=m, attribute=Attribute(m.arg1), attribute_level=m.arg2, rarity=rarity),
- ModifierIdentifier.HeadpieceGenericAttribute: lambda m, _, rarity: HeadpieceGenericAttribute(modifier=m, rarity=rarity),
- ModifierIdentifier.HealthRegeneneration: lambda m, _, rarity: HealthRegeneneration(modifier=m, health_regeneration=-m.arg2, rarity=rarity),
- ModifierIdentifier.HealthMinus: lambda m, _, rarity: HealthMinus(modifier=m, health_reduction=m.arg2, rarity=rarity),
- ModifierIdentifier.HealthPlus: lambda m, _, rarity: HealthPlus(modifier=m, health=m.arg1, rarity=rarity),
- ModifierIdentifier.HealthPlus2 : lambda m, _, rarity: HealthPlus(modifier=m, health=m.arg2, rarity=rarity),
- ModifierIdentifier.HealthPlusEnchanted: lambda m, _, rarity: HealthPlusEnchanted(modifier=m, health=m.arg1, rarity=rarity),
- ModifierIdentifier.HealthPlusHexed: lambda m, _, rarity: HealthPlusHexed(modifier=m, health=m.arg1, rarity=rarity),
- ModifierIdentifier.HealthPlusStance: lambda m, _, rarity: HealthPlusStance(modifier=m, health=m.arg1, rarity=rarity),
- ModifierIdentifier.HealthStealOnHit: lambda m, _, rarity: HealthStealOnHit(modifier=m, health_steal=m.arg1, rarity=rarity),
- ModifierIdentifier.HighlySalvageable: lambda m, _, rarity: HighlySalvageable(modifier=m, rarity=rarity),
- ModifierIdentifier.IncreaseConditionDuration: lambda m, _, rarity: IncreaseConditionDuration(modifier=m, condition=Ailment(m.arg2), rarity=rarity),
- ModifierIdentifier.IncreaseEnchantmentDuration: lambda m, _, rarity: IncreaseEnchantmentDuration(modifier=m, enchantment_duration=m.arg2, rarity=rarity),
- ModifierIdentifier.IncreasedSaleValue: lambda m, _, rarity: IncreasedSaleValue(modifier=m, rarity=rarity),
- ModifierIdentifier.Infused: lambda m, _, rarity: Infused(modifier=m, rarity=rarity),
- ModifierIdentifier.OfTheProfession: lambda m, _, rarity: OfTheProfession(modifier=m, attribute=Attribute(m.arg1), attribute_level=m.arg2, profession=get_profession_from_attribute(Attribute(m.arg1)) or Profession._None, rarity=rarity),
- ModifierIdentifier.ReceiveLessPhysDamageEnchanted: lambda m, _, rarity: ReceiveLessPhysDamageEnchanted(modifier=m, damage_reduction=m.arg2, rarity=rarity),
- ModifierIdentifier.ReceiveLessPhysDamageHexed: lambda m, _, rarity: ReceiveLessPhysDamageHexed(modifier=m, damage_reduction=m.arg2, rarity=rarity),
- ModifierIdentifier.ReceiveLessPhysDamageStance: lambda m, _, rarity: ReceiveLessPhysDamageStance(modifier=m, damage_reduction=m.arg2, rarity=rarity),
- ModifierIdentifier.ReduceConditionDuration: lambda m, _, rarity: ReduceConditionDuration(modifier=m, condition=Reduced_Ailment(m.arg1), rarity=rarity),
- ModifierIdentifier.ReduceConditionTupleDuration: lambda m, _, rarity: ReduceConditionTupleDuration(modifier=m, condition_1=Reduced_Ailment(m.arg2), condition_2=Reduced_Ailment(m.arg1), rarity=rarity),
- ModifierIdentifier.ReducesDiseaseDuration: lambda m, _, rarity: ReducesDiseaseDuration(modifier=m, rarity=rarity),
- ModifierIdentifier.ReceiveLessDamage: lambda m, _, rarity: ReceiveLessDamage(modifier=m, damage_reduction=m.arg2, chance=m.arg1, rarity=rarity),
- ModifierIdentifier.TargetItemType: lambda m, _, rarity: TargetItemTypeProperty(modifier=m, item_type=ItemType(m.arg1), rarity=rarity),
- ModifierIdentifier.TooltipDescription: lambda m, _, rarity: TooltipProperty(modifier=m, rarity=rarity),
-
- ModifierIdentifier.AttributeRune: lambda m, mods, rarity:
- get_upgrade_property(m, mods, ItemUpgradeType.Prefix, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.Inscription, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.Suffix, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.UpgradeRune, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.AppliesToRune, rarity) or
- UnknownUpgradeProperty(modifier=m, upgrade_id=m.upgrade_id, rarity=rarity),
-
- ModifierIdentifier.Upgrade: lambda m, mods, rarity:
- get_upgrade_property(m, mods, ItemUpgradeType.Prefix, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.Inscription, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.Suffix, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.UpgradeRune, rarity) or
- get_upgrade_property(m, mods, ItemUpgradeType.AppliesToRune, rarity) or
- UnknownUpgradeProperty(modifier=m, upgrade_id=m.upgrade_id, rarity=rarity),
- }
diff --git a/Py4GWCoreLib/item_mods_src/upgrades.py b/Py4GWCoreLib/item_mods_src/upgrades.py
index 3ea3d459c..ed8aa23c4 100644
--- a/Py4GWCoreLib/item_mods_src/upgrades.py
+++ b/Py4GWCoreLib/item_mods_src/upgrades.py
@@ -154,8 +154,8 @@ def _humanize_identifier(name: str) -> str:
return re.sub(r"(? Optional["Upgrade"]:
#region Encoded String Generation
def get_text_color(self, name : bool = False) -> bytes:
- match self.rarity:
- case Rarity.Blue | Rarity.White:
- return GWEncoded.ITEM_ENHANCE if name else GWEncoded.ITEM_BONUS
-
- case Rarity.Purple:
- return GWEncoded.ITEM_UNCOMMON
-
- case Rarity.Gold:
- return GWEncoded.ITEM_RARE
-
- case Rarity.Green:
- return GWEncoded.ITEM_UNIQUE
+ return GWStringEncoded.get_rarity_bytes(self.rarity, name)
def create_upgrade_name(self, item_type: ItemType) -> GWStringEncoded:
encoded_name = self.create_encoded_name()
diff --git a/Py4GWCoreLib/native_src/internals/encoded_strings.py b/Py4GWCoreLib/native_src/internals/encoded_strings.py
index 553531eca..12bb6edf3 100644
--- a/Py4GWCoreLib/native_src/internals/encoded_strings.py
+++ b/Py4GWCoreLib/native_src/internals/encoded_strings.py
@@ -2,12 +2,12 @@
import struct
from typing import Optional
from Py4GWCoreLib.enums_src.GameData_enums import Ailment, Attribute, DamageType, Profession, Reduced_Ailment
-from Py4GWCoreLib.enums_src.Item_enums import ItemType
+from Py4GWCoreLib.enums_src.Item_enums import ItemType, Rarity
from Py4GWCoreLib.native_src.internals import string_table
from Py4GWCoreLib.item_mods_src.types import ItemBaneSpecies
class GWStringEncoded:
- COLOR_TAG_RE = re.compile(r"]+>(.*?)")
+ COLOR_TAG_RE = re.compile(r"]+>(.*?)", re.IGNORECASE)
STAT_TAGS = (
"",
"",
@@ -16,13 +16,15 @@ class GWStringEncoded:
def __init__(self, encoded: bytes | list[int], fallback: str, placeholder_bytes: bytes = bytes(), placeholder_replacement: Optional[list[str]] = None):
self.encoded = bytes(encoded) if isinstance(encoded, list) else encoded
- self.fallback = fallback
+ self._fallback = fallback
self.placeholder_bytes = placeholder_bytes
self.placeholder_replacement = placeholder_replacement
self.__plain = ""
+ self.__plain_singular = ""
self.__bonuses_only = ""
self.__full = ""
self.__singular = ""
+ self.__plain_singular = ""
def decode(self) -> str:
decoded = string_table.decode(self.encoded)
@@ -30,12 +32,13 @@ def decode(self) -> str:
decoded = decoded.replace(string_table.decode(self.placeholder_bytes), "").strip()
return decoded if decoded else ""
-
- def decode_with_amount(self, amount: int) -> str:
+
+ def decode_with_amount(self, amount: int, plain: bool = False, rarity : Optional[Rarity] = None) -> str:
if amount < 1:
raise ValueError("Encoded string amounts must be non-negative.")
-
+
encoded = bytes([
+ *GWStringEncoded.get_rarity_bytes(rarity=rarity, name=True),
*GWEncoded.NUM1_STR1,
0x1, 0x1,
*GWEncoded._encode_string_table_number(amount),
@@ -43,7 +46,7 @@ def decode_with_amount(self, amount: int) -> str:
*self.encoded,
0x1, 0x0,
])
- decoded = string_table.decode_plain(encoded)
+ decoded = string_table.decode_plain(encoded) if plain else string_table.decode(encoded)
return decoded if decoded else ""
def get_decoded(self) -> str:
@@ -69,6 +72,11 @@ def remove_placeholder(self, s: str) -> str:
return s.strip()
+ @property
+ def fallback(self) -> str:
+ ''' Returns the fallback string to use when decoding fails or results in an empty string. This is useful for ensuring that there is always a meaningful string to display, even if the encoded data cannot be properly decoded. '''
+ return self._fallback ##+ " (FALLBACK)"
+
@property
def plain(self) -> str:
''' Returns the decoded string with color tags removed. This is useful for general display purposes where color formatting is not needed or desired. The result is cached after the first decoding for performance. If the encoded string cannot be decoded, the fallback value is returned. '''
@@ -121,18 +129,84 @@ def singular(self) -> str:
if not decoded:
return self.fallback
- if '[s]' in decoded or '[pl:' in decoded:
+ if '[f:' not in decoded and GWStringEncoded.__has_bracket_pair(decoded):
decoded = self.decode_with_amount(1)
self.__singular = self.remove_placeholder(decoded)
return self.__singular
+ @property
+ def plain_singular(self) -> str:
+ ''' Returns the singular form of the decoded string, if applicable. This is useful for item names that may have a plural form in the encoded string but need to be displayed in singular form (e.g., "Birthday Cupcake" instead of "137 Birthday Cupcakes").
+ \nThe method checks for specific patterns in the decoded string to determine if it should attempt to convert it to singular form. The result is cached after the first decoding for performance. If the encoded string cannot be decoded, the fallback value is returned. '''
+ if not self.__plain_singular:
+ decoded = self.decode()
+
+ if not decoded:
+ return self.fallback
+
+ if '[f:' not in decoded and GWStringEncoded.__has_bracket_pair(decoded):
+ decoded = self.decode_with_amount(1)
+
+ plain = self.remove_placeholder(decoded)
+ plain = self.remove_placeholder(self.COLOR_TAG_RE.sub(r"\1", decoded))
+ self.__plain_singular = plain
+
+ return self.__plain_singular
+
def with_amount(self, amount: int = 1) -> str:
''' Returns the decoded string with the specified amount inserted, if the encoded string is designed to include an amount. This is useful for item names that include a quantity (e.g., "137 Birthday Cupcakes") and allows you to get the correctly formatted string for any given amount.
\nIf the encoded string does not support amounts, it will simply return the plain decoded string with the amount prefixed. '''
return self.decode_with_amount(amount) or f"{amount} {self.plain}"
+
+ @staticmethod
+ def __has_bracket_pair(s: str) -> bool:
+ try:
+ start = s.index('[')
+ end = s.index(']', start + 1)
+ # if the brackets are more than 3 characters apart, it's unlikely to be a valid item name format, so we can assume it's not meant to be singularized unless it contains the plural indicator '[pl:'
+ return end - start < 3 or '[pl:' in s
+ except ValueError:
+ return False
+
+
+ @staticmethod
+ def _parse_name_encoded(value: Optional[str]) -> bytes:
+ if not value:
+ return bytes()
+ value = value.strip()
+ if not value:
+ return bytes()
+
+ parts = [part.strip() for part in value.split(",") if part.strip()]
+ return bytes(int(part, 16) for part in parts)
+
+ @staticmethod
+ def _format_name_encoded(value: bytes | list[int]) -> str:
+ if not value:
+ return ""
+
+ return ", ".join(f"0x{byte:X}" for byte in value)
+
+ @staticmethod
+ def get_rarity_bytes(rarity : Optional[Rarity] = None, name : bool = False) -> bytes:
+ match rarity:
+ case Rarity.Blue | Rarity.White:
+ return GWEncoded.ITEM_ENHANCE if name else GWEncoded.ITEM_BONUS
+
+ case Rarity.Purple:
+ return GWEncoded.ITEM_UNCOMMON
+
+ case Rarity.Gold:
+ return GWEncoded.ITEM_RARE
+
+ case Rarity.Green:
+ return GWEncoded.ITEM_UNIQUE
+
+ return bytes()
+
class GWEncoded():
@@ -319,15 +393,27 @@ class GWEncoded():
DamageType.unknown_15: bytes([]),
}
- ITEM_BASIC = bytes([0x2, 0x0, 0x3B, 0xA, 0xA, 0x1])
- ITEM_BONUS = bytes([0x2, 0x0, 0x3C, 0xA, 0xA, 0x1])
- ITEM_COMMON = bytes([0x2, 0x0, 0x3D, 0xA, 0xA, 0x1])
- ITEM_DULL = bytes([0x2, 0x0, 0x3E, 0xA, 0xA, 0x1])
- ITEM_ENHANCE = bytes([0x2, 0x0, 0x3F, 0xA, 0xA, 0x1])
- ITEM_RARE = bytes([0x2, 0x0, 0x40, 0xA, 0xA, 0x1])
- ITEM_RESTRICT = bytes([0x2, 0x0, 0x41, 0xA, 0xA, 0x1])
- ITEM_UNCOMMON = bytes([0x2, 0x0, 0x42, 0xA, 0xA, 0x1])
- ITEM_UNIQUE = bytes([0x2, 0x0, 0x43, 0xA, 0xA, 0x1])
+ ITEM_BASIC = bytes([0x3B, 0xA, 0xA, 0x1])
+ ITEM_BONUS = bytes([0x3C, 0xA, 0xA, 0x1])
+ ITEM_COMMON = bytes([0x3D, 0xA, 0xA, 0x1])
+ ITEM_DULL = bytes([0x3E, 0xA, 0xA, 0x1])
+ ITEM_ENHANCE = bytes([0x3F, 0xA, 0xA, 0x1])
+ ITEM_RARE = bytes([0x40, 0xA, 0xA, 0x1])
+ ITEM_RESTRICT = bytes([0x41, 0xA, 0xA, 0x1])
+ ITEM_UNCOMMON = bytes([0x42, 0xA, 0xA, 0x1])
+ ITEM_UNIQUE = bytes([0x43, 0xA, 0xA, 0x1])
+
+ ITEM_MARKDOWNS = [
+ ITEM_BASIC,
+ ITEM_BONUS,
+ ITEM_COMMON,
+ ITEM_DULL,
+ ITEM_ENHANCE,
+ ITEM_RARE,
+ ITEM_RESTRICT,
+ ITEM_UNCOMMON,
+ ITEM_UNIQUE,
+ ]
PLUS_NUM_TEMPLATE = bytes([0x84, 0xA, 0xA, 0x1])
PLUS_PERCENT_TEMPLATE = bytes([0x85, 0xA, 0xA, 0x1])
diff --git a/Py4GWCoreLib/native_src/internals/string_table.py b/Py4GWCoreLib/native_src/internals/string_table.py
index d6dbb8b69..038ec2261 100644
--- a/Py4GWCoreLib/native_src/internals/string_table.py
+++ b/Py4GWCoreLib/native_src/internals/string_table.py
@@ -228,7 +228,7 @@ def _decode_entry(
_INLINE_GENDER_TAG_RE = re.compile(r'\[(f|m):"([^"]*)"\]')
_INLINE_GENDER_PAIR_RE = re.compile(r'\[(m|f):"([^"]*)"\]\[(m|f):"([^"]*)"\]')
_INDEFINITE_ARTICLE_RE = re.compile(r'\[(?:a|an)\](\s+)([A-Za-zÀ-ÖØ-öø-ÿ][A-Za-zÀ-ÖØ-öø-ÿ\'-]*)')
-_COLOR_TAG_RE = re.compile(r"]+>(.*?)")
+_COLOR_TAG_RE = re.compile(r"]+>(.*?)", re.IGNORECASE)
def _apply_inline_gender_tags(text: str, prefer_male: bool = True) -> str: