diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7ce601375f..2adbf8c91c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +* Worked around a bug in the Bungie data that had all armor as "unknown" class instead of Hunter/Titan/Warlock. + ## 8.57.1 (2025-02-03) * DIM Sync data is now loaded incrementally, instead of being completely refreshed every time. This should result in faster updates, but otherwise nothing should be different. If you notice things are out of sync, you can click the "Reload remote data from DIM sync" button in Settings, but please let us know if you needed to do that. diff --git a/src/app/inventory/__snapshots__/d2-stores.test.ts.snap b/src/app/inventory/__snapshots__/d2-stores.test.ts.snap index 8e6d75c6c2..972edc35a6 100644 --- a/src/app/inventory/__snapshots__/d2-stores.test.ts.snap +++ b/src/app/inventory/__snapshots__/d2-stores.test.ts.snap @@ -36,7 +36,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Seasonal Mod": [ "lastwish", ], - "Source": "lastwish", + "Source": "", "Strength": 4, "Strength (Base)": 2, "Tag": undefined, @@ -2160,7 +2160,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 12, "Season": 4, "Seasonal Mod": "", - "Source": "campaign", + "Source": "", "Strength": 0, "Strength (Base)": 0, "Tag": undefined, @@ -2349,7 +2349,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 8, "Season": 4, "Seasonal Mod": "", - "Source": "campaign", + "Source": "", "Strength": 0, "Strength (Base)": 0, "Tag": undefined, @@ -2898,7 +2898,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 34, "Season": 4, "Seasonal Mod": "", - "Source": "campaign", + "Source": "", "Strength": 0, "Strength (Base)": 0, "Tag": undefined, @@ -6469,7 +6469,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 13, "Season": 11, "Seasonal Mod": "", - "Source": "legendaryengram", + "Source": "", "Strength": 2, "Strength (Base)": 2, "Tag": undefined, @@ -7539,7 +7539,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 12, "Season": 4, "Seasonal Mod": "", - "Source": "campaign", + "Source": "", "Strength": 7, "Strength (Base)": 7, "Tag": undefined, @@ -7611,7 +7611,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 13, "Season": 4, "Seasonal Mod": "", - "Source": "campaign", + "Source": "", "Strength": 2, "Strength (Base)": 2, "Tag": undefined, @@ -7683,7 +7683,7 @@ exports[`process stores generates a correct armor CSV export 1`] = ` "Resilience (Base)": 6, "Season": 4, "Seasonal Mod": "", - "Source": "campaign", + "Source": "", "Strength": 2, "Strength (Base)": 2, "Tag": undefined, @@ -10723,7 +10723,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": true, - "Mag": 36, + "Mag": 35, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Extraordinary Rendition", @@ -11295,7 +11295,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Void", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, @@ -12264,7 +12264,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Reload": 44, "Season": 15, "Shield Duration": 0, - "Source": "ironbanner", + "Source": "", "Stability": 95, "Swing Speed": 0, "Tag": undefined, @@ -12463,7 +12463,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, @@ -12753,7 +12753,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 77, @@ -13170,7 +13170,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 381, "Loadouts": "", "Locked": true, - "Mag": 5, + "Mag": 6, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "The Enigma", @@ -14016,7 +14016,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Reload": 29, "Season": 16, "Shield Duration": 0, - "Source": "ironbanner", + "Source": "", "Stability": 80, "Swing Speed": 0, "Tag": undefined, @@ -14572,7 +14572,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, @@ -14699,7 +14699,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 502, "Loadouts": "", "Locked": true, - "Mag": 7, + "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Nezarec's Whisper", @@ -15011,7 +15011,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Reload": 32, "Season": 16, "Shield Duration": 0, - "Source": "ironbanner", + "Source": "", "Stability": 79, "Swing Speed": 0, "Tag": undefined, @@ -15267,7 +15267,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Arc", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, @@ -16622,7 +16622,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 568, "Loadouts": "", "Locked": true, - "Mag": 4, + "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Shield Duration", "Name": "Judgment of Kelgorath", @@ -16737,7 +16737,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": true, - "Mag": 45, + "Mag": 44, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "The Manticore", @@ -17552,7 +17552,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, @@ -17800,7 +17800,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": true, - "Mag": 39, + "Mag": 38, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Pizzicato-22", @@ -18266,7 +18266,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, @@ -18561,7 +18561,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Void", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 81, @@ -18918,7 +18918,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 335, "Loadouts": "", "Locked": true, - "Mag": 6, + "Mag": 7, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Vexcalibur", @@ -20322,7 +20322,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 19, "Loadouts": "", "Locked": true, - "Mag": 37, + "Mag": 45, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Centrifuse", @@ -20956,7 +20956,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, @@ -21730,7 +21730,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": false, - "Mag": 39, + "Mag": 38, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Pizzicato-22", @@ -22307,7 +22307,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": true, - "Mag": 74, + "Mag": 194, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Divinity", @@ -23568,7 +23568,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": false, - "Mag": 6, + "Mag": 7, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Edge of Concurrence", @@ -24198,7 +24198,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Charge Time": 0, "Crafted": false, "Crafted Level": 0, - "Draw Time": 540, + "Draw Time": 500, "Element": "Void", "Equipped": false, "Event": "", @@ -25013,7 +25013,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Kinetic", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, @@ -25972,7 +25972,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": true, - "Mag": 3, + "Mag": 4, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Winterbite", @@ -26323,7 +26323,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 4, "Loadouts": "", "Locked": true, - "Mag": 41, + "Mag": 42, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Subjunctive", @@ -27356,7 +27356,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Solar", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, @@ -27537,7 +27537,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Solar", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, @@ -27772,7 +27772,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Arc", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 40, "Guard Resistance": 40, "Handling": 0, @@ -28009,7 +28009,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Arc", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 27, @@ -28078,7 +28078,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": false, - "Mag": 5, + "Mag": 6, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Greasy Luck", @@ -28717,7 +28717,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Element": "Arc", "Equipped": false, "Event": "", - "Foundry": undefined, + "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, @@ -28967,7 +28967,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": false, - "Mag": 45, + "Mag": 44, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "The Recluse", @@ -29279,7 +29279,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` ], "Power": 1923, "ROF": 100, - "Range": 30, + "Range": 0, "Recoil": 75, "Reload": 45, "Season": 24, @@ -29428,7 +29428,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": true, - "Mag": 41, + "Mag": 42, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Parabellum", @@ -29828,7 +29828,7 @@ exports[`process stores generates a correct weapon CSV export 1`] = ` "Kill Tracker": 0, "Loadouts": "", "Locked": false, - "Mag": 39, + "Mag": 38, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Pizzicato-22", diff --git a/src/app/inventory/store/d2-item-factory.ts b/src/app/inventory/store/d2-item-factory.ts index ef312f7d5c..14bfc98bb4 100644 --- a/src/app/inventory/store/d2-item-factory.ts +++ b/src/app/inventory/store/d2-item-factory.ts @@ -7,6 +7,8 @@ import { SOME_OTHER_DUMMY_BUCKET, THE_FORBIDDEN_BUCKET, d2MissingIcon, + infusionCategoryHashToClass, + plugCategoryHashToClass, uniqueEquipBuckets, } from 'app/search/d2-known-values'; import { lightStats } from 'app/search/search-filter-values'; @@ -27,6 +29,7 @@ import { DestinyItemResponse, DestinyItemSubType, DestinyItemTooltipNotification, + DestinyItemType, DestinyObjectiveProgress, DestinyProfileResponse, DestinyVendorSaleItemComponent, @@ -434,17 +437,52 @@ export function makeItem( } } - // we cannot trust the claimed class of redacted items. they all say Titan - const classType = itemDef.redacted - ? normalBucket.inArmor + let classType = itemDef.classType; + + // We cannot trust the defined class of redacted items, + // and adjust their claimed classType based on their status in the user's inventory. + if (itemDef.redacted) { + classType = normalBucket.inArmor ? itemInstanceData?.isEquipped && owner ? // equipped armor gets marked as that character's class owner.classType - : // unequipped armor gets marked "no class" + : // unequipped armor gets marked "no class" (assume/allow nothing) DestinyClass.Classified - : // other items are marked "any class" - DestinyClass.Unknown - : itemDef.classType; + : // other items are marked "any class"/unknown + DestinyClass.Unknown; + } else if (itemDef.classType === DestinyClass.Unknown) { + // This whole elseif can be removed once Bungie addresses https://github.com/Bungie-net/api/issues/1937 and restores item class information. + // However, the heuristics are strict, and this only adjusts Unknown armor, so there's no rush to remove it. We also need to re-enable the + // test in loadout-drawer-reducer.test.ts. + if ( + itemDef.itemType === DestinyItemType.Armor || + // Festival masks are head armor in traits but not types. + (itemDef.itemType === DestinyItemType.None && + itemDef.traitHashes?.includes(TraitHashes.ItemArmorHead)) + ) { + // This identifies most armors (90%+) by infusion category. + classType = + infusionCategoryHashToClass[itemDef.quality!.infusionCategoryHash] ?? DestinyClass.Unknown; + + // This identifies the remaining armors by what class' armor they can act as an ornament for. + if (classType === DestinyClass.Unknown) { + // If this item has a plug, it can be an ornament. If not, find a visually matching ornament item. + const plugCheckItem = itemDef.plug + ? itemDef + : Object.values(defs.InventoryItem.getAll()).find( + (i) => i.plug && i.displayProperties.icon === itemDef.displayProperties.icon, + ); + + if (plugCheckItem) { + classType = + plugCategoryHashToClass[plugCheckItem.plug!.plugCategoryHash] ?? DestinyClass.Unknown; + } + } + } else if (itemDef.itemType === DestinyItemType.Subclass && owner) { + // Obviously a subclass is compatible with the Guardian holding it. + classType = owner.classType; + } + } const createdItem: DimItem = { owner: owner?.id || 'unknown', @@ -486,7 +524,7 @@ export function makeItem( maxStackSize: Math.max(itemDef.inventory!.maxStackSize, 1), uniqueStack: Boolean(itemDef.inventory!.stackUniqueLabel?.length), classType, - classTypeNameLocalized: getClassTypeNameLocalized(defs)(itemDef.classType), + classTypeNameLocalized: getClassTypeNameLocalized(defs)(classType), element, energy: itemInstanceData.energy ?? null, lockable: normalBucket.hash !== BucketHashes.Finishers ? item.lockable : true, diff --git a/src/app/loadout-drawer/loadout-drawer-reducer.test.ts b/src/app/loadout-drawer/loadout-drawer-reducer.test.ts index 413c652e35..1a7f44c080 100644 --- a/src/app/loadout-drawer/loadout-drawer-reducer.test.ts +++ b/src/app/loadout-drawer/loadout-drawer-reducer.test.ts @@ -21,7 +21,7 @@ import { toggleEquipped, updateMods, } from './loadout-drawer-reducer'; -import { filterLoadoutToAllowedItems, newLoadout } from './loadout-utils'; +import { newLoadout } from './loadout-utils'; let defs: D2ManifestDefinitions; let store: DimStore; @@ -217,16 +217,17 @@ describe('addItem', () => { expect(loadout.items).toEqual([]); }); - it('removes class-specific items when saving as "any class"', () => { - const hunterItem = allItems.find((i) => i.classType === DestinyClass.Hunter)!; - expect(hunterItem).toBeDefined(); + // TODO: reenable after https://github.com/Bungie-net/api/issues/1937 is fixed + // it('removes class-specific items when saving as "any class"', () => { + // const hunterItem = allItems.find((i) => i.classType === DestinyClass.Hunter)!; + // expect(hunterItem).toBeDefined(); - let loadout = addItem(defs, hunterItem)(emptyLoadout); - loadout = setClassType(DestinyClass.Unknown)(loadout); - loadout = filterLoadoutToAllowedItems(defs, loadout); + // let loadout = addItem(defs, hunterItem)(emptyLoadout); + // loadout = setClassType(DestinyClass.Unknown)(loadout); + // loadout = filterLoadoutToAllowedItems(defs, loadout); - expect(loadout.items).toEqual([]); - }); + // expect(loadout.items).toEqual([]); + // }); it('does nothing if the bucket is already at capacity', () => { const weapons = items.filter((i) => i.bucket.hash === BucketHashes.KineticWeapons); diff --git a/src/app/search/d2-known-values.ts b/src/app/search/d2-known-values.ts index ad812da0eb..928f7bf9dc 100644 --- a/src/app/search/d2-known-values.ts +++ b/src/app/search/d2-known-values.ts @@ -1,6 +1,6 @@ import { CustomStatWeights } from '@destinyitemmanager/dim-api-types'; import { HashLookup } from 'app/utils/util-types'; -import { TierType } from 'bungie-api-ts/destiny2'; +import { DestinyClass, TierType } from 'bungie-api-ts/destiny2'; import { BreakerTypeHashes, @@ -318,3 +318,45 @@ export const enum ModsWithConditionalStats { } export const ARTIFICE_PERK_HASH = 3727270518; // InventoryItem "Artifice Armor" + +/** Most armor's Guardian Class can be determined by what it can be infused into */ +export const infusionCategoryHashToClass: Record = { + 3301232851: DestinyClass.Hunter, // hunter head + 3853570272: DestinyClass.Hunter, // hunter arms + 1814923096: DestinyClass.Hunter, // hunter chest + 2542340350: DestinyClass.Hunter, // hunter legs + 821835303: DestinyClass.Hunter, // hunter class + + 3548569221: DestinyClass.Titan, // titan head + 3647016162: DestinyClass.Titan, // titan arms + 2858080654: DestinyClass.Titan, // titan chest + 1462658336: DestinyClass.Titan, // titan legs + 4159907129: DestinyClass.Titan, // titan class + + 1046741990: DestinyClass.Warlock, // warlock head + 673964473: DestinyClass.Warlock, // warlock arms + 3312934999: DestinyClass.Warlock, // warlock chest + 3441938495: DestinyClass.Warlock, // warlock legs + 2278270520: DestinyClass.Warlock, // warlock class +}; + +/** Most remaining armor's Guardian Class can be determined by its ornament category */ +export const plugCategoryHashToClass: Record = { + [PlugCategoryHashes.ArmorSkinsHunterLegs]: DestinyClass.Hunter, + [PlugCategoryHashes.ArmorSkinsHunterChest]: DestinyClass.Hunter, + [PlugCategoryHashes.ArmorSkinsHunterHead]: DestinyClass.Hunter, + [PlugCategoryHashes.ArmorSkinsHunterClass]: DestinyClass.Hunter, + [PlugCategoryHashes.ArmorSkinsHunterArms]: DestinyClass.Hunter, + + [PlugCategoryHashes.ArmorSkinsWarlockHead]: DestinyClass.Warlock, + [PlugCategoryHashes.ArmorSkinsWarlockLegs]: DestinyClass.Warlock, + [PlugCategoryHashes.ArmorSkinsWarlockArms]: DestinyClass.Warlock, + [PlugCategoryHashes.ArmorSkinsWarlockChest]: DestinyClass.Warlock, + [PlugCategoryHashes.ArmorSkinsWarlockClass]: DestinyClass.Warlock, + + [PlugCategoryHashes.ArmorSkinsTitanHead]: DestinyClass.Titan, + [PlugCategoryHashes.ArmorSkinsTitanChest]: DestinyClass.Titan, + [PlugCategoryHashes.ArmorSkinsTitanLegs]: DestinyClass.Titan, + [PlugCategoryHashes.ArmorSkinsTitanArms]: DestinyClass.Titan, + [PlugCategoryHashes.ArmorSkinsTitanClass]: DestinyClass.Titan, +};