message: Resolve loadout items based on craftedDate
diff: @@ -454,7 +454,12 @@ const matchByHash = [
  export function getResolutionInfo(
  defs: D1ManifestDefinitions | D2ManifestDefinitions,
  loadoutItemHash: number
  -) {
  +):
  + | {
  + hash: number;
  + instanced: boolean;
  + }
  + | undefined {
  const hash = oldToNewItems[loadoutItemHash] ?? loadoutItemHash;
  const def = defs.InventoryItem.get(hash) as
  @@ -472,10 +477,11 @@ export function getResolutionInfo(
  // nice to use "is random rolled or configurable" here instead but that's hard
  // to determine.
  const bucketHash = def.bucketTypeHash || def.inventory?.bucketTypeHash || 0;
  - const instanced =
  + const instanced = Boolean(
  (def.instanced || def.inventory?.isInstanceItem) &&
  // Subclasses and some other types are technically instanced but should be matched by hash
  - !matchByHash.includes(bucketHash);
  + !matchByHash.includes(bucketHash)
  + );
  return {
  hash,
  @@ -490,13 +496,19 @@ export function getResolutionInfo(
  export function findSameLoadoutItemIndex(
  defs: D1ManifestDefinitions | D2ManifestDefinitions,
  loadoutItems: LoadoutItem[],
  - loadoutItem: Pick<LoadoutItem, 'hash' | 'id'>
  + loadoutItem: Pick<LoadoutItem, 'hash' | 'id' | 'craftedDate'>
  ) {
  const info = getResolutionInfo(defs, loadoutItem.hash)!;
  return loadoutItems.findIndex((i) => {
  const newHash = oldToNewItems[i.hash] ?? i.hash;
  - return info.hash === newHash && (!info.instanced || loadoutItem.id === i.id);
  + return (
  + info.hash === newHash &&
  + (!info.instanced ||
  + loadoutItem.id === i.id ||
  + // Crafted items may change ID but keep their date
  + (loadoutItem.craftedDate && loadoutItem.craftedDate === i.craftedDate))
  + );
  });
  }
  @@ -517,7 +529,15 @@ export function findItemForLoadout(
  // TODO: so inefficient to look through all items over and over again - need an index by ID and hash
  if (info.instanced) {
  - return allItems.find((item) => item.id === loadoutItem.id);
  + const result = allItems.find((item) => item.id === loadoutItem.id);
  + if (result) {
  + return result;
  + }
  +
  + // Crafted items get new IDs, but keep their crafted date, so we can match on that
  + if (loadoutItem.craftedDate) {
  + return allItems.find((item) => item.craftedInfo?.craftedDate === loadoutItem.craftedDate);
  + }
  }
  return getUninstancedLoadoutItem(allItems, info.hash, storeId);
  @@ -542,24 +562,9 @@ export function isMissingItems(
  loadout: Loadout
  ): boolean {
  for (const loadoutItem of loadout.items) {
  - const info = getResolutionInfo(defs, loadoutItem.hash);
  - if (!info) {
  - // If an item hash is entirely missing from the database, we show that
  - // there is a missing item but can't offer a replacement (or even show
  - // which item went missing), but we can maybe add a migration in `oldToNewItems`?
  - return true;
  - }
  - if (info.instanced) {
  - if (!allItems.some((item) => item.id === loadoutItem.id)) {
  + if (!findItemForLoadout(defs, allItems, storeId, loadoutItem)) {
  return true;
  }
  - } else {
  - // The vault can't really have uninstanced items like subclasses or emblems, so no point
  - // in reporting a missing item in that case.
  - if (storeId !== 'vault' && !getUninstancedLoadoutItem(allItems, info.hash, storeId)) {
  - return true;
  - }
  - }
  }
  return false;
  }
  