diff --git a/src/main/java/studio/magemonkey/divinity/utils/DivinityProvider.java b/src/main/java/studio/magemonkey/divinity/utils/DivinityProvider.java index 9a58d397..d994a50f 100644 --- a/src/main/java/studio/magemonkey/divinity/utils/DivinityProvider.java +++ b/src/main/java/studio/magemonkey/divinity/utils/DivinityProvider.java @@ -18,9 +18,12 @@ import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; import studio.magemonkey.divinity.stats.items.ItemStats; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public class DivinityProvider implements ICodexItemProvider { public static final String NAMESPACE = "DIVINITY"; @@ -82,12 +85,18 @@ public DivinityItemType getItem(String id) { IModule module = Divinity.getInstance().getModuleManager().getModule(split[0]); if (!(module instanceof QModuleDrop)) return null; moduleItem = ((QModuleDrop) module).getItemById(split[1]); - } else { // Look in all modules - for (IModule module : Divinity.getInstance().getModuleManager().getModules()) { - if (!(module instanceof QModuleDrop)) continue; + } else { // Look in all modules; require the id to be unambiguous + Map, ModuleItem> matches = findModuleItemsById(id); + + if (matches.size() > 1) { + Codex.error("Ambiguous Divinity item id '" + id + "' found in multiple modules (" + + matches.keySet().stream().map(IModule::getId).collect(Collectors.joining(", ")) + + "). Refer to it as ':" + id + "' to disambiguate."); + return null; + } - moduleItem = ((QModuleDrop) module).getItemById(id); - if (moduleItem != null) break; + if (!matches.isEmpty()) { + moduleItem = matches.values().iterator().next(); } } @@ -96,6 +105,20 @@ public DivinityItemType getItem(String id) { return null; } + /** + * Finds every module that has an item registered under the given plain id. + */ + private static Map, ModuleItem> findModuleItemsById(String id) { + Map, ModuleItem> matches = new LinkedHashMap<>(); + for (IModule module : Divinity.getInstance().getModuleManager().getModules()) { + if (!(module instanceof QModuleDrop)) continue; + + ModuleItem candidate = ((QModuleDrop) module).getItemById(id); + if (candidate != null) matches.put(module, candidate); + } + return matches; + } + @Override @Nullable public DivinityProvider.DivinityItemType getItem(ItemStack itemStack) { @@ -119,12 +142,12 @@ public boolean isCustomItemOfId(ItemStack item, String id) { String itemId = ItemStats.getId(item); if (itemId == null) return false; + if (itemId.equals(id)) return true; + // Backward compatibility: older callers may still pass the legacy + // "module:id" namespaced form this method used to require. String[] split = id.split(":", 2); - if (split.length < 2) { - return itemId.equals(id); - } - + if (split.length < 2) return false; QModuleDrop module = ItemStats.getModule(item); return module != null && module.getId().equalsIgnoreCase(split[0]) && itemId.equals(split[1]); } @@ -158,7 +181,11 @@ public String getNamespace() { @Override public String getID() { - return this.moduleItem.getModule().getId() + ":" + this.moduleItem.getId(); + String id = this.moduleItem.getId(); + if (findModuleItemsById(id).size() > 1) { + return this.moduleItem.getModule().getId() + ":" + id; + } + return id; } @Override diff --git a/src/test/java/studio/magemonkey/divinity/utils/DivinityProviderTest.java b/src/test/java/studio/magemonkey/divinity/utils/DivinityProviderTest.java index 4e8712c3..ecea0070 100644 --- a/src/test/java/studio/magemonkey/divinity/utils/DivinityProviderTest.java +++ b/src/test/java/studio/magemonkey/divinity/utils/DivinityProviderTest.java @@ -1,17 +1,16 @@ package studio.magemonkey.divinity.utils; import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import org.bukkit.inventory.ItemStack; import studio.magemonkey.codex.Codex; import studio.magemonkey.codex.CodexEngine; import studio.magemonkey.codex.items.CodexItemManager; import studio.magemonkey.codex.modules.ModuleManager; import studio.magemonkey.divinity.Divinity; -import studio.magemonkey.divinity.modules.api.QModuleDrop; import studio.magemonkey.divinity.modules.list.arrows.ArrowManager; import studio.magemonkey.divinity.modules.list.customitems.CustomItemsManager; import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; @@ -62,13 +61,14 @@ void setUp() { itemGenModule = spy(new ItemGeneratorManager(divinity)); when(moduleManager.getModule("item_generator")).thenReturn(itemGenModule); - when(moduleManager.getModules()).thenReturn(List.of(arrowModule, itemGenModule)); doReturn("item_generator").when(itemGenModule).getId(); customItemsModule = spy(new CustomItemsManager(divinity)); when(moduleManager.getModule("custom_items")).thenReturn(customItemsModule); doReturn("custom_items").when(customItemsModule).getId(); + when(moduleManager.getModules()).thenReturn(List.of(arrowModule, itemGenModule, customItemsModule)); + //noinspection unchecked when(divinity.getModuleManager()).thenReturn(moduleManager); } @@ -83,8 +83,6 @@ void afterEach() { void getItem_usesLevel() { ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); - when(generatorItem.getId()).thenReturn("foobar"); - doReturn((QModuleDrop) itemGenModule).when(generatorItem).getModule(); DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_item_generator:foobar~level:5"); @@ -100,8 +98,6 @@ void getItem_usesLevel() { void getItem_usesMaterial() { ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); - when(generatorItem.getId()).thenReturn("foobar"); - doReturn((QModuleDrop) itemGenModule).when(generatorItem).getModule(); DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_item_generator:foobar~material:VANILLA_DIAMOND"); @@ -119,8 +115,6 @@ void getItem_usesMaterial() { void getItem_noModule_returnsItem() { ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); - when(generatorItem.getId()).thenReturn("foobar"); - doReturn((QModuleDrop) itemGenModule).when(generatorItem).getModule(); DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_foobar"); @@ -136,8 +130,6 @@ void getItem_noModule_returnsItem() { void getItem_customItems_returnsItem() { CustomItemsManager.CustomItem codexItem = mock(CustomItemsManager.CustomItem.class); doReturn(codexItem).when(customItemsModule).getItemById("foobar"); - when(codexItem.getId()).thenReturn("foobar"); - doReturn((QModuleDrop) customItemsModule).when(codexItem).getModule(); DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_custom_items:foobar"); @@ -149,19 +141,6 @@ void getItem_customItems_returnsItem() { assertInstanceOf(DivinityProvider.DivinityItemType.class, item); } - @Test - void getItem_namespacedIdIncludesModule() { - CustomItemsManager.CustomItem codexItem = mock(CustomItemsManager.CustomItem.class); - doReturn(codexItem).when(customItemsModule).getItemById("foobar"); - when(codexItem.getId()).thenReturn("foobar"); - doReturn((QModuleDrop) customItemsModule).when(codexItem).getModule(); - - DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_custom_items:foobar"); - - assertNotNull(item); - assertEquals("DIVINITY_custom_items:foobar", item.getNamespacedID()); - } - @Test void getItem_itemStackUsesStoredModule() { ItemStack itemStack = mock(ItemStack.class); @@ -184,4 +163,77 @@ void getItem_itemStackUsesStoredModule() { verify(itemGenModule, never()).getItemById("foobar"); } } + + @Test + void getItem_itemStackWithoutStoredModule_fallsBackToBareIdLookup() { + ItemStack itemStack = mock(ItemStack.class); + + ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); + doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); + + try (MockedStatic itemStats = mockStatic(ItemStats.class)) { + itemStats.when(() -> ItemStats.getId(itemStack)).thenReturn("foobar"); + itemStats.when(() -> ItemStats.getModule(itemStack)).thenReturn(null); + + DivinityProvider.DivinityItemType item = provider.getItem(itemStack); + + assertNotNull(item); + assertEquals(generatorItem, item.getModuleItem()); + } + } + + @Test + void getItem_ambiguousBareId_returnsNull() { + ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); + doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); + + CustomItemsManager.CustomItem codexItem = mock(CustomItemsManager.CustomItem.class); + doReturn(codexItem).when(customItemsModule).getItemById("foobar"); + + DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_foobar"); + + assertNull(item); + } + + @Test + void getItem_ambiguousId_stillResolvableWithModulePrefix() { + ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); + doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); + + CustomItemsManager.CustomItem codexItem = mock(CustomItemsManager.CustomItem.class); + doReturn(codexItem).when(customItemsModule).getItemById("foobar"); + + DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_custom_items:foobar"); + + assertNotNull(item); + assertEquals(codexItem, item.getModuleItem()); + } + + @Test + void getID_uniqueId_returnsBareId() { + CustomItemsManager.CustomItem codexItem = mock(CustomItemsManager.CustomItem.class); + doReturn(codexItem).when(customItemsModule).getItemById("foobar"); + when(codexItem.getId()).thenReturn("foobar"); + + DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_custom_items:foobar"); + + assertNotNull(item); + assertEquals("foobar", item.getID()); + } + + @Test + void getID_ambiguousId_returnsModulePrefixedId() { + ItemGeneratorManager.GeneratorItem generatorItem = mock(ItemGeneratorManager.GeneratorItem.class); + doReturn(generatorItem).when(itemGenModule).getItemById("foobar"); + + CustomItemsManager.CustomItem codexItem = mock(CustomItemsManager.CustomItem.class); + doReturn(codexItem).when(customItemsModule).getItemById("foobar"); + when(codexItem.getId()).thenReturn("foobar"); + doReturn(customItemsModule).when(codexItem).getModule(); + + DivinityProvider.DivinityItemType item = provider.getItem("DIVINITY_custom_items:foobar"); + + assertNotNull(item); + assertEquals("custom_items:foobar", item.getID()); + } }