diff --git a/CP2077SaveEditor/Views/Controls/InventoryControl.cs b/CP2077SaveEditor/Views/Controls/InventoryControl.cs index 1f4d0cf..9018112 100644 --- a/CP2077SaveEditor/Views/Controls/InventoryControl.cs +++ b/CP2077SaveEditor/Views/Controls/InventoryControl.cs @@ -85,6 +85,213 @@ private void clearQuestFlagsButton_Click(object sender, EventArgs e) MessageBox.Show("All item flags cleared."); } + /// + /// Checks if an item is safe to consider for duplicate removal + /// + private bool IsSafeToRemoveDuplicates(ItemData item) + { + var itemId = item.ItemInfo.ItemId.Id.ResolvedText; + + if (string.IsNullOrEmpty(itemId)) + return false; + + // Currently only clothing items are considered safe (for wardrobe cleanup) + return itemId.StartsWith("Items.Formal") || + itemId.StartsWith("Items.Casual") || + itemId.StartsWith("Items.Boots") || + itemId.StartsWith("Items.Jacket") || + itemId.StartsWith("Items.Pants") || + itemId.StartsWith("Items.Shirt") || + itemId.StartsWith("Items.Shoes") || + itemId.StartsWith("Items.Skirt") || + itemId.StartsWith("Items.Dress") || + itemId.StartsWith("Items.Hat") || + itemId.StartsWith("Items.Glasses") || + itemId.StartsWith("Items.Mask") || + itemId.StartsWith("Items.Vest") || + itemId.StartsWith("Items.Shorts") || + itemId.StartsWith("Items.Sweater") || + itemId.StartsWith("Items.Tank") || + itemId.StartsWith("Items.Top") || + itemId.StartsWith("Items.Underwear") || + itemId.StartsWith("Items.Bra") || + itemId.StartsWith("Items.Panties"); + } + + /// + /// Items are considered duplicates if they have the same base item, quality level, and mods. + /// + private string GetItemUniqueKey(ItemData item) + { + var key = item.ItemInfo.ItemId.Id.ResolvedText; + + // Add quality information if available (this is what determines legendary vs common) + if (item.ItemAdditionalInfo != null) + { + key += $"|LootPool:{item.ItemAdditionalInfo.LootItemPoolId}"; + key += $"|Level:{item.ItemAdditionalInfo.RequiredLevel}"; + } + + // Add item structure information (affects how item behaves) + key += $"|Structure:{item.ItemInfo.ItemStructure}"; + + // Add flags (quest items, etc. - these matter for functionality) + key += $"|Flags:{item.Flags}"; + + // Add quantity for stackable items (different quantities are different items) + key += $"|Qty:{item.Quantity}"; + + return key; + } + + /// + /// Keeps the first occurrence of each unique item and removes all subsequent duplicates. + /// + private void RemoveDuplicates(object sender, EventArgs e) + { + // Get the currently selected inventory + var currentContainerId = containersListBox.SelectedItem?.ToString(); + if (string.IsNullOrEmpty(currentContainerId)) + { + MessageBox.Show("Please select an inventory first.", "No Inventory Selected", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + // Convert display name back to ID if needed + if (_inventoryNames.Values.Contains(currentContainerId)) + { + currentContainerId = _inventoryNames.FirstOrDefault(x => x.Value == currentContainerId).Key.ToString(); + } + + // Get the current inventory name for display + var currentInventoryName = _inventoryNames.ContainsKey(ulong.Parse(currentContainerId)) + ? _inventoryNames[ulong.Parse(currentContainerId)] + : $"Inventory {currentContainerId}"; + + // Display warning message including current limitations + var dialogMessage = $@"Choose duplicate removal scope: + + Yes - Remove duplicates from ALL inventories + No - Remove duplicates from {currentInventoryName} only + Cancel - Abort + + WARNING: This is a dangerous operation! For safety, this currently + only removes clothing duplicates (for wardrobe cleanup)."; + + var result = MessageBox.Show(dialogMessage, "Remove Duplicates", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); + + if (result == DialogResult.Cancel) + { + return; + } + + var removeFromAll = result == DialogResult.Yes; + var totalRemoved = 0; + var duplicatesFound = new Dictionary(); + var inventoriesProcessed = new List(); + + var inventoriesToProcess = removeFromAll + ? _parentForm.ActiveSaveFile.GetInventoriesContainer().SubInventories + : new List { + _parentForm.ActiveSaveFile.GetInventory(ulong.Parse(currentContainerId)) + }.Where(x => x != null); + + foreach (SubInventory inventory in inventoriesToProcess) + { + if (inventory == null) continue; + + var inventoryName = _inventoryNames.ContainsKey(inventory.InventoryId) + ? _inventoryNames[inventory.InventoryId] + : $"Inventory {inventory.InventoryId:X}"; + + var itemsToRemove = new List(); + var seenItems = new HashSet(); + + foreach (ItemData item in inventory.Items) + { + var itemId = item.ItemInfo.ItemId.Id.ResolvedText; + + // Only process items that are safe to remove duplicates from + if (!IsSafeToRemoveDuplicates(item)) + { + continue; + } + + var itemUniqueKey = GetItemUniqueKey(item); + + if (seenItems.Contains(itemUniqueKey)) + { + // This is a duplicate, mark for removal + itemsToRemove.Add(item); + totalRemoved++; + + if (duplicatesFound.ContainsKey(itemId)) + { + duplicatesFound[itemId]++; + } + else + { + duplicatesFound[itemId] = 2; // First duplicate found + } + } + else + { + seenItems.Add(itemUniqueKey); + } + } + + // Remove the duplicate items + foreach (var itemToRemove in itemsToRemove) + { + inventory.Items.Remove(itemToRemove); + } + + if (itemsToRemove.Count > 0) + { + inventoriesProcessed.Add($"{inventoryName}: {itemsToRemove.Count} duplicates removed"); + } + } + + // Show results + var scope = removeFromAll ? "all inventories" : $"{currentInventoryName.ToLower()} only"; + var message = $"Duplicate removal complete!\n\nScope: {scope}\nTotal duplicates removed: {totalRemoved}"; + + if (duplicatesFound.Count > 0) + { + message += $"\n\nItems with duplicates found: {duplicatesFound.Count}"; + if (duplicatesFound.Count <= 15) // Show details for reasonable numbers + { + message += "\n\nDuplicate items:"; + foreach (var kvp in duplicatesFound.OrderByDescending(x => x.Value)) + { + message += $"\n• {kvp.Key} ({kvp.Value} copies)"; + } + } + else + { + message += $"\n\nTop 10 most duplicated items:"; + foreach (var kvp in duplicatesFound.OrderByDescending(x => x.Value).Take(10)) + { + message += $"\n• {kvp.Key} ({kvp.Value} copies)"; + } + } + } + + if (inventoriesProcessed.Count > 0) + { + message += "\n\nInventories processed:"; + foreach (var inv in inventoriesProcessed) + { + message += $"\n• {inv}"; + } + } + + MessageBox.Show(message, "Duplicate Removal Results", MessageBoxButtons.OK, MessageBoxIcon.Information); + + // Refresh the inventory display + RefreshInventory(); + } + private void debloatButton_Click(object sender, EventArgs e) { if (MessageBox.Show("This process will remove redundant data from your save. Just in case, it's recommended that you back up your save before continuing. Continue?", "Notice", MessageBoxButtons.YesNo) != DialogResult.Yes) @@ -420,6 +627,9 @@ private void inventoryListView_MouseDown(object sender, MouseEventArgs e) contextMenu.Items.Add("Delete", null, DeleteInventoryItem).Tag = hitTest.Item; } + var removeDuplicatesItem = contextMenu.Items.Add("Remove Duplicates"); + removeDuplicatesItem.Click += RemoveDuplicates; + contextMenu.Show(Cursor.Position); } }