Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion scripts/slice-test-bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,31 @@
"# to Dexterity",
"# to Intelligence",
"#% increased Stun and Block Recovery",
# Nebulis on a synthesised base: three implicits with a per-unique fixed magnitude and two
# unique-only explicits, one of them shared by no other stat in the slice at all. The case
# for the base line's "Synthesised " prefix, which is not part of any base's own name and
# has to come off before "Void Sceptre" resolves.
"#% increased Critical Strike Chance",
"#% increased Chaos Damage",
"Bleeding you inflict deals Damage #% faster",
"#% increased Implicit Modifier magnitudes",
"#% increased Cast Speed",
"#% increased Elemental Damage per 1% Fire, Cold, or Lightning Resistance above 75%",
# A rare Heist Gear item's affixes. Grants Level # is what proves the suffix reads past its
# own tier line; the other two also cover the two boilerplate lines Heist Gear prints around
# its affixes ("Any Heist member can equip this item.", "Can only be equipped to Heist
# members."), which carry no roll and must not be read as unmatched modifiers.
"#% increased Projectile Attack Damage",
"# to # added Lightning Damage\n"
"Players and their Minions have # to # added Lightning Damage",
"Grants Level # Purity of Ice Skill",
# Four Heist Contract affixes with no roll at all, which is what the game marks with a
# title-case, em-dash "Unscalable Value" rather than the lowercase, parenthesized suffix a
# numeric roll gets — a wording no matcher above already covers.
"Monsters are Hexproof",
"Reward Rooms have #% increased Monsters",
"Monsters Poison on Hit",
"The Ring takes no Cut",
]

ITEMS = [
Expand Down Expand Up @@ -288,6 +313,15 @@
"ITEM::Onyx Amulet",
"UNIQUE::Bound Fate",
"ITEM::Cloth Belt",
# A synthesised base and the unique that rolls on it. The base line the client prints is
# "Synthesised Void Sceptre", never a name of its own — the record here is filed under
# "Void Sceptre" alone, which is the case for stripping the prefix before lookup.
"ITEM::Void Sceptre",
"UNIQUE::Nebulis",
# A Heist Gear base, rare rather than unique — the case for the class's own boilerplate
# lines ("Any Heist member can equip this item.", "Can only be equipped to Heist members.")
# reading as usage text rather than as unmatched modifiers.
"ITEM::Precise Arrowhead",
]

UNIQUE_MODS = [
Expand All @@ -297,6 +331,7 @@
"The Dark Monarch",
"Replica Dragonfang's Flight",
"Bound Fate",
"Nebulis",
]

# Keyed on the first of each entry's mod ids, which is stable and is what the debug log names.
Expand All @@ -321,12 +356,19 @@
"HeistContractBurningGround",
"HeistContractBurningGround1",
"HeistContractMonsterPatrolAdditionalElite1",
# The four "Unscalable Value" affixes above, so map check can rate a contract that rolls
# them rather than drawing it unrateable.
"HeistContractHexproof",
"HeistContractSideAreaIncreasedMonsters1_",
"HeistContractPoisoning",
"HeistContractNoGangCut1",
]

ITEM_CLASSES = ["Rings", "Boots", "Gloves", "Body Armours", "Stackable Currency",
"Divination Cards", "Jewels", "Utility Flasks", "Maps", "Skill Gems",
"Support Gems", "Chart", "Misc Map Items", "Contracts", "Blueprints",
"Sanctum Research", "Expedition Logbooks", "Helmets", "Amulets", "Belts"]
"Sanctum Research", "Expedition Logbooks", "Helmets", "Amulets", "Belts",
"Sceptres", "Heist Gear"]

LANG = "en"

Expand Down
14 changes: 11 additions & 3 deletions src/data/lexicon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace {
/// translates and nothing else, so these are the whole of its vocabulary.
constexpr std::array<std::string_view, static_cast<size_t>(Term::Count)> kTermKeys{
"item_class_label", "rarity_label", "requirements_label", "sockets_label",
"note_label", "superior_prefix", "map_tier_prefix", "blighted_map",
"note_label", "superior_prefix", "synthesised_prefix", "map_tier_prefix", "blighted_map",
"blight_ravaged_map", "foulborn_prefix", "foulborn_word", "vaal_prefix",
"influence_suffix", "tier_prefix", "rank_prefix", "modifier_word",
"prefix_word", "suffix_word", "unique_word", "increased_word",
Expand All @@ -26,7 +26,7 @@ constexpr std::array<std::string_view, static_cast<size_t>(Term::Count)> kTermKe
constexpr std::array<std::string_view, static_cast<size_t>(TermList::Count)> kListKeys{
"rarities", "influences", "flags", "mod_suffixes", "generations", "value_annotations",
"usage_needles", "quest_rarity", "chart_shapes", "ultimatum_challenges",
"ultimatum_rewards", "heist_jobs", "heist_objective_values"};
"ultimatum_rewards", "heist_jobs", "heist_objective_values", "unscalable_suffixes"};

struct PropertyName {
std::string_view key;
Expand Down Expand Up @@ -123,6 +123,7 @@ void Lexicon::assign_english() {
set(terms_, Term::SocketsLabel, "Sockets");
set(terms_, Term::NoteLabel, "Note");
set(terms_, Term::SuperiorPrefix, "Superior ");
set(terms_, Term::SynthesisedPrefix, "Synthesised ");
set(terms_, Term::MapTierPrefix, " (Tier ");
set(terms_, Term::BlightedMap, "Blighted Map");
set(terms_, Term::BlightRavagedMap, "Blight-ravaged Map");
Expand Down Expand Up @@ -188,7 +189,12 @@ void Lexicon::assign_english() {
// opening with "Take this item", the same shape a chart's does — and without a needle
// of its own it came back as a fourth unrecognised modifier, since a logbook is gear
// and gear needs a positive signal before prose is read as anything but a mod.
"Dannig"};
"Dannig",
// Heist Gear prints two of its own boilerplate lines that carry no roll: "Any Heist
// member can equip this item." above the requirements, and "Can only be equipped to
// Heist members." at the bottom. Neither reads as a mod section on its own signal, so
// both came back as unrecognised modifiers — one needle catches both.
"Heist member"};
// "Quest Item" and "Divination Card" are printed with a trailing noun on some items, so
// the rarity line is matched on a prefix as well as whole.
lists_[static_cast<size_t>(TermList::QuestRarity)] = {"Quest"};
Expand All @@ -205,6 +211,8 @@ void Lexicon::assign_english() {
"Trap Disarmament", "Agility", "Deception", "Engineering"};
lists_[static_cast<size_t>(TermList::HeistObjectiveValues)] = {
"Moderate Value", "High Value", "Precious", "Priceless"};
lists_[static_cast<size_t>(TermList::UnscalableSuffixes)] = {
" (unscalable value)", " \xe2\x80\x94 Unscalable Value", " - Unscalable Value"};

quality_prefix_ = "Quality (";
properties_ = {
Expand Down
9 changes: 9 additions & 0 deletions src/data/lexicon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ enum class Term : uint8_t {
NoteLabel, ///< "Note"
SuperiorPrefix, ///< "Superior ", worn by a quality item nothing else has named — a white
///< one or any unidentified one — and known to no lookup
SynthesisedPrefix,///< "Synthesised ", how the client prints a synthesised base's own type
///< line — the bundle knows the base only under the name beneath it
MapTierPrefix, ///< " (Tier ", closed by ')' — "Map (Tier 16)"
BlightedMap, ///< the whole base line of a blighted map
BlightRavagedMap,
Expand Down Expand Up @@ -200,6 +202,13 @@ enum class TermList : uint8_t {
/// the game prints in the parenthetical after the target's name, so "Precious" and not
/// "(Precious)".
HeistObjectiveValues,
/// free — what the game appends to a roll item level cannot scale: a numeric one gets a
/// lowercase parenthetical, a modifier with no roll at all (a Heist Contract's own boolean
/// effects among them, "Monsters are Hexproof") gets an em-dash "Unscalable Value" instead,
/// and Wine's clipboard fallback can turn that em dash into a plain hyphen the same way it
/// does for an info line's tags (see `kEmDash` in parse.cpp) — three spellings of one
/// wording, so a suffix can be added here without touching the code that checks for it.
UnscalableSuffixes,
Count
};

Expand Down
19 changes: 13 additions & 6 deletions src/data/stat_matcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,18 @@ namespace {
/// always wins, because the loop returns at the first `end` that resolves.
constexpr size_t kMaxModLines = 8;

/// The game appends this to a roll that item level cannot scale.
constexpr std::string_view kUnscalableSuffix = " (unscalable value)";
/// Strips whichever spelling of "this roll is fixed" the lexicon's `UnscalableSuffixes` list
/// carries `line` ending in — see that list for why there is more than one. Sets `unscalable`
/// and shortens `line` when found.
bool strip_unscalable_suffix(std::string_view& line, const Lexicon& lex) {
for (const std::string& suffix : lex.list(TermList::UnscalableSuffixes)) {
if (!suffix.empty() && line.size() > suffix.size() && line.ends_with(suffix)) {
line.remove_suffix(suffix.size());
return true;
}
}
return false;
}

double pow10i(int n) {
double p = 1.0;
Expand Down Expand Up @@ -54,10 +64,7 @@ std::optional<StatMatch> match_stat(const GameData& gd, std::span<const std::str
for (size_t end = start; end < lines.size() && end - start < kMaxModLines; ++end) {
std::string_view line = lines[end];
if (is_reminder_text(line)) continue;
if (line.size() > kUnscalableSuffix.size() && line.ends_with(kUnscalableSuffix)) {
line.remove_suffix(kUnscalableSuffix.size());
unscalable = true;
}
if (strip_unscalable_suffix(line, gd.lexicon())) unscalable = true;
if (!join.empty()) join.push_back('\n');
join.append(line);

Expand Down
7 changes: 7 additions & 0 deletions src/item/parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@ void parse_header(const Section& sec, Item& it, const data::Lexicon& lex) {
const std::string_view superior = lex.term(data::Term::SuperiorPrefix);
if (!superior.empty() && it.base_type.starts_with(superior))
it.base_type.erase(0, superior.size());
// A synthesised weapon or armour piece prints "Synthesised " ahead of its base's own type
// line, and the bundle carries the base only under the name beneath it — "Void Sceptre",
// never "Synthesised Void Sceptre". Superior can still be outermost ("Superior Synthesised
// Void Sceptre"), which is why this runs after that strip rather than instead of it.
const std::string_view synthesised = lex.term(data::Term::SynthesisedPrefix);
if (!synthesised.empty() && it.base_type.starts_with(synthesised))
it.base_type.erase(0, synthesised.size());
// A map prints its tier on the base line — "Map (Tier 16)", and on a magic one after the
// affixes, "Map of Impedance (Tier 16)". It is the thing the map is searched on and it is
// not part of the base's name, which is a bare "Map" in every bundle and on trade.
Expand Down
Binary file modified tests/data/bundle/en-items-base.index.bin
Binary file not shown.
Binary file modified tests/data/bundle/en-items-name.index.bin
Binary file not shown.
Binary file modified tests/data/bundle/en-items-ref.index.bin
Binary file not shown.
3 changes: 3 additions & 0 deletions tests/data/bundle/en-items.ndjson
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@
{"craftable":{"category":"Amulets"},"domain":1,"dropLevel":25,"h":1,"metadataId":"Metadata/Items/Amulets/Amulet7","name":"Onyx Amulet","namespace":"ITEM","refName":"Onyx Amulet","w":1}
{"art":"Art/2DItems/Belts/HinekoraBelt.png","name":"Bound Fate","namespace":"UNIQUE","refName":"Bound Fate","unique":{"base":"Cloth Belt"}}
{"craftable":{"category":"Belts"},"domain":1,"dropLevel":20,"h":1,"metadataId":"Metadata/Items/Belts/Belt5","name":"Cloth Belt","namespace":"ITEM","refName":"Cloth Belt","w":2}
{"craftable":{"category":"Sceptres"},"domain":1,"dropLevel":68,"h":3,"metadataId":"Metadata/Items/Weapons/OneHandWeapons/OneHandMaces/Sceptre22","name":"Void Sceptre","namespace":"ITEM","refName":"Void Sceptre","w":2}
{"art":"Art/2DItems/Weapons/OneHandWeapons/Scepters/SceptreOfMemories.png","name":"Nebulis","namespace":"UNIQUE","refName":"Nebulis","unique":{"base":"Void Sceptre"}}
{"craftable":{"category":"Heist Gear"},"domain":23,"dropLevel":83,"h":2,"metadataId":"Metadata/Items/Heist/HeistEquipmentRanged4","name":"Precise Arrowhead","namespace":"ITEM","refName":"Precise Arrowhead","w":2}
Binary file modified tests/data/bundle/en-mod-pools-ref.index.bin
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/data/bundle/en-mod-pools.ndjson
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
{"domain":22,"gen":2,"mods":["HeistContractBurningGround"],"stats":[{"ref":"Area has patches of Burning Ground"}],"tiers":1}
{"domain":22,"gen":2,"mods":["HeistContractBurningGround1","HeistContractBurningGround2","HeistContractBurningGround3___","HeistContractBurningGround4"],"name":"of Flames","stats":[{"ref":"Area has patches of Burning Ground"},{"max":-4,"min":-4,"ref":"#% more raising of Alert Level"},{"max":4,"min":4,"ref":"#% increased time before Lockdown"}],"tiers":4}
{"domain":22,"gen":1,"mods":["HeistContractMonsterPatrolAdditionalElite1","HeistContractMonsterPatrolAdditionalElite2","HeistContractMonsterPatrolAdditionalElite3","HeistContractMonsterPatrolAdditionalElite4","HeistContractMonsterPatrolAdditionalElite5"],"name":"Elite","stats":[{"max":30,"min":10,"ref":"Patrol Packs have #% increased chance to be replaced by an Elite Patrol Pack","trade":"explicit.stat_1982436039"},{"max":-7,"min":-7,"ref":"#% more raising of Alert Level"},{"max":7,"min":7,"ref":"#% increased time before Lockdown"}],"tiers":5}
{"domain":22,"gen":1,"mods":["HeistContractHexproof"],"name":"Hexproof","stats":[{"ref":"Monsters are Hexproof","trade":"explicit.stat_4154059009"},{"max":-6,"min":-6,"ref":"#% more raising of Alert Level"},{"max":6,"min":6,"ref":"#% increased time before Lockdown"}],"tiers":1}
{"domain":22,"gen":1,"mods":["HeistContractSideAreaIncreasedMonsters1_","HeistContractSideAreaIncreasedMonsters2__","HeistContractSideAreaIncreasedMonsters3_","HeistContractSideAreaIncreasedMonsters4","HeistContractSideAreaIncreasedMonsters5"],"name":"Deployed","stats":[{"max":30,"min":13,"ref":"Reward Rooms have #% increased Monsters","trade":"explicit.stat_4056408881"},{"max":-5,"min":-5,"ref":"#% more raising of Alert Level"},{"max":5,"min":5,"ref":"#% increased time before Lockdown"}],"tiers":5}
{"domain":22,"gen":2,"mods":["HeistContractPoisoning"],"name":"of Venom","stats":[{"ref":"Monsters Poison on Hit"},{"max":-6,"min":-6,"ref":"#% more raising of Alert Level"},{"max":6,"min":6,"ref":"#% increased time before Lockdown"}],"tiers":1}
{"domain":22,"gen":2,"mods":["HeistContractNoGangCut1"],"name":"of Selfishness","stats":[{"ref":"The Ring takes no Cut","trade":"explicit.stat_616993076"},{"max":0,"min":0,"ref":"#% more raising of Alert Level"},{"max":0,"min":0,"ref":"#% increased time before Lockdown"}],"tiers":1}
Binary file modified tests/data/bundle/en-stats-matcher.index.bin
Binary file not shown.
Binary file modified tests/data/bundle/en-stats-ref.index.bin
Binary file not shown.
13 changes: 13 additions & 0 deletions tests/data/bundle/en-stats.ndjson
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,16 @@
{"better":1,"matchers":[{"string":"# to Dexterity"}],"ref":"# to Dexterity","trade":{"ids":{"crafted":["crafted.stat_3261801346"],"explicit":["explicit.stat_3261801346"],"fractured":["fractured.stat_3261801346"],"implicit":["implicit.stat_3261801346"],"scourge":["scourge.stat_3261801346"]}}}
{"better":1,"matchers":[{"string":"# to Intelligence"}],"ref":"# to Intelligence","trade":{"ids":{"crafted":["crafted.stat_328541901"],"explicit":["explicit.stat_328541901"],"fractured":["fractured.stat_328541901"],"implicit":["implicit.stat_328541901"],"scourge":["scourge.stat_328541901"]}}}
{"better":1,"matchers":[{"string":"#% increased Stun and Block Recovery"},{"negate":true,"string":"#% reduced Stun and Block Recovery"}],"ref":"#% increased Stun and Block Recovery","trade":{"ids":{"explicit":["explicit.stat_2511217560"],"fractured":["fractured.stat_2511217560"],"implicit":["implicit.stat_2511217560"],"scourge":["scourge.stat_2511217560"]}}}
{"better":1,"matchers":[{"string":"#% increased Critical Strike Chance"},{"negate":true,"string":"#% reduced Critical Strike Chance"}],"ref":"#% increased Critical Strike Chance","trade":{"ids":{"crafted":["crafted.stat_2375316951"],"enchant":["enchant.stat_2375316951"],"explicit":["explicit.stat_2375316951"],"fractured":["fractured.stat_2375316951"],"implicit":["implicit.stat_2375316951"]}}}
{"better":1,"matchers":[{"string":"#% increased Chaos Damage"},{"negate":true,"string":"#% reduced Chaos Damage"}],"ref":"#% increased Chaos Damage","trade":{"ids":{"crafted":["crafted.stat_736967255"],"enchant":["enchant.stat_736967255"],"explicit":["explicit.stat_736967255"],"fractured":["fractured.stat_736967255"],"implicit":["implicit.stat_736967255"],"scourge":["scourge.stat_736967255"]}}}
{"better":1,"matchers":[{"string":"Bleeding you inflict deals Damage #% faster"}],"ref":"Bleeding you inflict deals Damage #% faster","trade":{"ids":{"explicit":["explicit.stat_3828375170"],"fractured":["fractured.stat_3828375170"],"implicit":["implicit.stat_3828375170"],"scourge":["scourge.stat_3828375170"]}}}
{"better":1,"matchers":[{"string":"#% increased Implicit Modifier magnitudes"},{"negate":true,"string":"#% reduced Implicit Modifier magnitudes"}],"ref":"#% increased Implicit Modifier magnitudes","trade":{"ids":{"explicit":["explicit.stat_2304729532"]}}}
{"better":1,"matchers":[{"string":"#% increased Cast Speed"},{"negate":true,"string":"#% reduced Cast Speed"}],"ref":"#% increased Cast Speed","trade":{"ids":{"crafted":["crafted.stat_2891184298"],"enchant":["enchant.stat_2891184298"],"explicit":["explicit.stat_2891184298"],"fractured":["fractured.stat_2891184298"],"implicit":["implicit.stat_2891184298"],"scourge":["scourge.stat_2891184298"]}}}
{"better":1,"matchers":[{"string":"#% increased Elemental Damage per 1% Fire, Cold, or Lightning Resistance above 75%"}],"ref":"#% increased Elemental Damage per 1% Fire, Cold, or Lightning Resistance above 75%","trade":{"ids":{"explicit":["explicit.stat_1138456002"]}}}
{"better":1,"matchers":[{"string":"#% increased Projectile Attack Damage"},{"negate":true,"string":"#% reduced Projectile Attack Damage"}],"ref":"#% increased Projectile Attack Damage","trade":{"ids":{"explicit":["explicit.stat_2162876159"],"fractured":["fractured.stat_2162876159"],"implicit":["implicit.stat_2162876159"]}}}
{"better":1,"matchers":[{"string":"# to # added Lightning Damage\nPlayers and their Minions have # to # added Lightning Damage"}],"ref":"# to # added Lightning Damage\nPlayers and their Minions have # to # added Lightning Damage","trade":{"ids":{"explicit":["explicit.stat_2697534676"],"fractured":["fractured.stat_2697534676"]}}}
{"better":1,"matchers":[{"string":"Grants Level # Purity of Ice Skill"}],"ref":"Grants Level # Purity of Ice Skill","trade":{"ids":{"explicit":["explicit.stat_4193390599","explicit.stat_151975117"],"fractured":["fractured.stat_151975117"],"implicit":["implicit.stat_4193390599"]}}}
{"better":1,"matchers":[{"string":"Monsters are Hexproof"}],"ref":"Monsters are Hexproof","trade":{"ids":{"explicit":["explicit.stat_4154059009"],"fractured":["fractured.stat_4154059009"]}}}
{"better":1,"matchers":[{"string":"Reward Rooms have #% increased Monsters"},{"negate":true,"string":"Reward Rooms have #% reduced Monsters"}],"ref":"Reward Rooms have #% increased Monsters","trade":{"ids":{"explicit":["explicit.stat_4056408881"],"fractured":["fractured.stat_4056408881"]}}}
{"better":1,"matchers":[{"string":"Monsters Poison on Hit"}],"ref":"Monsters Poison on Hit","trade":{"ids":{"explicit":["explicit.stat_3350803563","explicit.stat_394639761"],"fractured":["fractured.stat_3350803563"]}}}
{"better":1,"matchers":[{"string":"The Ring takes no Cut"}],"ref":"The Ring takes no Cut","trade":{"ids":{"explicit":["explicit.stat_616993076"],"fractured":["fractured.stat_616993076"]}}}
Binary file modified tests/data/bundle/en-unique-mods-name.index.bin
Binary file not shown.
Loading