From 0bf8b5c734bec27ed2638e4f09c6aecaa7aadafe Mon Sep 17 00:00:00 2001 From: Danny Stewart Date: Sat, 6 Sep 2025 13:20:26 -0400 Subject: [PATCH 1/2] Add inventory duplicate removal feature - Add 'Remove Duplicates' option to inventory context menu - Automatically detects current inventory and offers relevant scope options - Removes duplicate items based on ItemId.ResolvedText matching - Provides detailed reporting of removed duplicates - Addresses common issue of duplicate items accumulating in Car Stash --- .../Views/Controls/InventoryControl.cs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/CP2077SaveEditor/Views/Controls/InventoryControl.cs b/CP2077SaveEditor/Views/Controls/InventoryControl.cs index 1f4d0cf..3646f69 100644 --- a/CP2077SaveEditor/Views/Controls/InventoryControl.cs +++ b/CP2077SaveEditor/Views/Controls/InventoryControl.cs @@ -85,6 +85,143 @@ private void clearQuestFlagsButton_Click(object sender, EventArgs e) MessageBox.Show("All item flags cleared."); } + /// + /// Removes duplicate items from inventories. Duplicates are identified by matching ItemId.ResolvedText. + /// 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}"; + + var result = MessageBox.Show($"Choose duplicate removal scope:\n\nYes - Remove duplicates from ALL inventories\nNo - Remove duplicates from {currentInventoryName} only\nCancel - Abort", + "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; + + if (string.IsNullOrEmpty(itemId)) + { + continue; // Skip items without valid IDs + } + + if (seenItems.Contains(itemId)) + { + // 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(itemId); + } + } + + // 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 +557,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); } } From e75f9c65d54eda2ee7ec877502942df12c639394 Mon Sep 17 00:00:00 2001 From: Danny Stewart Date: Mon, 8 Sep 2025 04:48:45 +0100 Subject: [PATCH 2/2] Limit duplicate removal to clothing for now - Added safety checks to limit duplicate removal to clothing items only for now (intended for wardrobe cleanup). - Added a method to generate an item key based on attributes to narrow uniqueness. - Updated the warning message to inform about current limitations of duplicate removal. --- .../Views/Controls/InventoryControl.cs | 84 +++++++++++++++++-- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/CP2077SaveEditor/Views/Controls/InventoryControl.cs b/CP2077SaveEditor/Views/Controls/InventoryControl.cs index 3646f69..9018112 100644 --- a/CP2077SaveEditor/Views/Controls/InventoryControl.cs +++ b/CP2077SaveEditor/Views/Controls/InventoryControl.cs @@ -86,7 +86,65 @@ private void clearQuestFlagsButton_Click(object sender, EventArgs e) } /// - /// Removes duplicate items from inventories. Duplicates are identified by matching ItemId.ResolvedText. + /// 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) @@ -110,8 +168,17 @@ private void RemoveDuplicates(object sender, EventArgs e) ? _inventoryNames[ulong.Parse(currentContainerId)] : $"Inventory {currentContainerId}"; - var result = MessageBox.Show($"Choose duplicate removal scope:\n\nYes - Remove duplicates from ALL inventories\nNo - Remove duplicates from {currentInventoryName} only\nCancel - Abort", - "Remove Duplicates", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); + // 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) { @@ -144,12 +211,15 @@ private void RemoveDuplicates(object sender, EventArgs e) { var itemId = item.ItemInfo.ItemId.Id.ResolvedText; - if (string.IsNullOrEmpty(itemId)) + // Only process items that are safe to remove duplicates from + if (!IsSafeToRemoveDuplicates(item)) { - continue; // Skip items without valid IDs + continue; } - if (seenItems.Contains(itemId)) + var itemUniqueKey = GetItemUniqueKey(item); + + if (seenItems.Contains(itemUniqueKey)) { // This is a duplicate, mark for removal itemsToRemove.Add(item); @@ -166,7 +236,7 @@ private void RemoveDuplicates(object sender, EventArgs e) } else { - seenItems.Add(itemId); + seenItems.Add(itemUniqueKey); } }