diff --git a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java index 29a68da..4f3d085 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java @@ -99,24 +99,30 @@ public void run() { } public FoodItem getMain() { - for(PlacedSlot slot : f.getActiveSlots().values()) { - ItemStack stack = slot.getCurrentItem(); - if(stack == null) continue; - FoodItem item = FoodItem.fromItem(stack); - if(item == null) continue; - return item; - } - return null; + PlacedSlot slot = firstFoodSlot(); + if (slot == null) return null; + return FoodItem.fromItem(slot.getCurrentItem()); } + /** Thickness lives on the first ingredient. Later pieces keep the model of what was held. */ public void setMain(FoodItem fi) { - for(PlacedSlot slot : f.getActiveSlots().values()) { + if (fi == null) return; + PlacedSlot slot = firstFoodSlot(); + if (slot == null) return; + ItemStack stack = slot.getCurrentItem(); + ItemStack updated = ItemUpdater.applyItemUpdate(stack, fi, f.getId()); + if (updated == null) return; + slot.forceModel(updated); + } + + private PlacedSlot firstFoodSlot() { + for (PlacedSlot slot : f.getActiveSlots().values()) { ItemStack stack = slot.getCurrentItem(); - if(stack == null) continue; - FoodItem item = FoodItem.fromItem(stack); - if(item == null) continue; - slot.forceModel(ItemUpdater.applyItemUpdate(stack, fi, f.getId())); + if (stack == null) continue; + if (FoodItem.fromItem(stack) == null) continue; + return slot; } + return null; } private void handleParticles() { diff --git a/src/main/java/net/tfminecraft/cooking/manager/BowlIngredientLayout.java b/src/main/java/net/tfminecraft/cooking/manager/BowlIngredientLayout.java new file mode 100644 index 0000000..cf6beec --- /dev/null +++ b/src/main/java/net/tfminecraft/cooking/manager/BowlIngredientLayout.java @@ -0,0 +1,35 @@ +package net.tfminecraft.cooking.manager; + +import net.tfminecraft.interactiblefurniture.furniture.data.DisplayData; + +/** + * Bowl ingredient slots share one centered offset in the furniture definition. + * Spread input_1..input_5 around the bowl interior when the soup is poured. + */ +public final class BowlIngredientLayout { + static final float RADIUS = 0.12f; + private static final int INPUTS = 5; + + private BowlIngredientLayout() {} + + public static DisplayData offsetFor(String slotId) { + Integer index = inputIndex(slotId); + if (index == null) return null; + double angle = -Math.PI / 2d + index * (2d * Math.PI / INPUTS); + DisplayData data = new DisplayData(); + data.setxPos((float) (Math.cos(angle) * RADIUS)); + data.setzPos((float) (Math.sin(angle) * RADIUS)); + return data; + } + + static Integer inputIndex(String slotId) { + if (slotId == null || !slotId.startsWith("input_")) return null; + try { + int index = Integer.parseInt(slotId.substring("input_".length())); + if (index < 1 || index > INPUTS) return null; + return index - 1; + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/src/main/java/net/tfminecraft/cooking/manager/PlateManager.java b/src/main/java/net/tfminecraft/cooking/manager/PlateManager.java index cff1d24..46faad6 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/PlateManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/PlateManager.java @@ -33,6 +33,7 @@ import net.tfminecraft.interactiblefurniture.events.FurnitureSlotItemAddEvent; import net.tfminecraft.interactiblefurniture.furniture.Furniture; import net.tfminecraft.interactiblefurniture.furniture.PlacedSlot; +import net.tfminecraft.interactiblefurniture.furniture.data.DisplayData; public class PlateManager implements Listener{ @@ -144,7 +145,12 @@ public void addSoup(Player p, Furniture f, FoodItem soup, ItemStack base) { for(Map.Entry entry : map.entrySet()) { if(f.hasActiveSlot(entry.getKey())) continue; if (f.getType() == null || f.getType().getSlot(entry.getKey()) == null) continue; - f.getOrCreatePlacedSlot(entry.getKey()).forceModel(entry.getValue()); + PlacedSlot placed = f.getOrCreatePlacedSlot(entry.getKey()); + placed.forceModel(entry.getValue()); + DisplayData spread = BowlIngredientLayout.offsetFor(entry.getKey()); + if (spread != null) { + placed.applyDisplayData(spread); + } } if (f.getType() == null || f.getType().getSlot("food_item") == null) return; f.getOrCreatePlacedSlot("food_item").forceModel(base); diff --git a/src/main/java/net/tfminecraft/cooking/utils/Encoder.java b/src/main/java/net/tfminecraft/cooking/utils/Encoder.java index c3c19f1..61137ca 100644 --- a/src/main/java/net/tfminecraft/cooking/utils/Encoder.java +++ b/src/main/java/net/tfminecraft/cooking/utils/Encoder.java @@ -5,6 +5,7 @@ import java.util.Map; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -22,15 +23,15 @@ public static String getEncodedSlots(Furniture f) { ItemMeta meta = item.getItemMeta(); int model = (meta != null && LegacyModelData.has(meta)) ? LegacyModelData.get(meta) : 0; + String itemModel = null; + if (meta != null && meta.hasItemModel() && meta.getItemModel() != null) { + itemModel = meta.getItemModel().toString(); + } if (!first) result.append(":"); first = false; - result.append(entry.getKey()) - .append(".") - .append(item.getType().toString()) - .append(".") - .append(model); + result.append(formatSlot(entry.getKey(), item.getType().toString(), model, itemModel)); } return result.toString(); @@ -47,37 +48,63 @@ public static Map decodeSlots(String encoded) { for (String part : parts) { if (part.isEmpty()) continue; - // Split into slot, type, model - String[] data = part.split("\\."); - if (data.length < 3) continue; // invalid format - - String slotId = data[0]; // "slot_1" - String materialName = data[1]; // "IRON_INGOT" - String modelStr = data[2]; // "5" + ParsedSlot parsed = parseSlot(part); + if (parsed == null) continue; - // Convert material - Material mat = Material.matchMaterial(materialName); - if (mat == null) continue; // ignore invalid material names - - int model; - try { - model = Integer.parseInt(modelStr); - } catch (NumberFormatException e) { - model = 0; // fallback - } + Material mat = Material.matchMaterial(parsed.material()); + if (mat == null) continue; - // Build item ItemStack item = new ItemStack(mat, 1); ItemMeta meta = item.getItemMeta(); if (meta != null) { - LegacyModelData.set(meta, model); + LegacyModelData.set(meta, parsed.customModelData()); + if (parsed.itemModel() != null) { + NamespacedKey key = NamespacedKey.fromString(parsed.itemModel()); + if (key != null) { + meta.setItemModel(key); + } + } item.setItemMeta(meta); } - // Add to result - result.put(slotId, item); + result.put(parsed.slotId(), item); } return result; } + + /** slot.MATERIAL.customModelData[.namespace~key]. ':' in the key is stored as '~'. */ + static String formatSlot(String slotId, String material, int customModelData, String itemModel) { + StringBuilder result = new StringBuilder(); + result.append(slotId).append('.').append(material).append('.').append(customModelData); + if (itemModel != null && !itemModel.isEmpty()) { + // Slot records are separated by ':'. Resource locations use ':' too. + result.append('.').append(itemModel.replace(':', '~')); + } + return result.toString(); + } + + static ParsedSlot parseSlot(String part) { + if (part == null || part.isEmpty()) return null; + String[] data = part.split("\\.", 4); + if (data.length < 3 || data[0].isEmpty() || data[1].isEmpty()) return null; + int model; + try { + model = Integer.parseInt(data[2]); + } catch (NumberFormatException e) { + model = 0; + } + String itemModel = null; + if (data.length >= 4 && !data[3].isEmpty()) { + int tilde = data[3].indexOf('~'); + if (tilde > 0) { + itemModel = data[3].substring(0, tilde) + ":" + data[3].substring(tilde + 1); + } else { + itemModel = data[3]; + } + } + return new ParsedSlot(data[0], data[1], model, itemModel); + } + + record ParsedSlot(String slotId, String material, int customModelData, String itemModel) {} } diff --git a/src/test/java/net/tfminecraft/cooking/manager/BowlIngredientLayoutTest.java b/src/test/java/net/tfminecraft/cooking/manager/BowlIngredientLayoutTest.java new file mode 100644 index 0000000..2ce9c72 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/manager/BowlIngredientLayoutTest.java @@ -0,0 +1,37 @@ +package net.tfminecraft.cooking.manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import net.tfminecraft.interactiblefurniture.furniture.data.DisplayData; + +class BowlIngredientLayoutTest { + + @Test + void ingredientsSitAroundTheBowlInsteadOfTheCenter() { + DisplayData[] placed = new DisplayData[5]; + for (int i = 1; i <= 5; i++) { + placed[i - 1] = BowlIngredientLayout.offsetFor("input_" + i); + double radius = Math.hypot(placed[i - 1].getxPos(), placed[i - 1].getzPos()); + assertEquals(BowlIngredientLayout.RADIUS, radius, 0.001); + } + for (int i = 0; i < placed.length; i++) { + for (int j = i + 1; j < placed.length; j++) { + double apart = Math.hypot( + placed[i].getxPos() - placed[j].getxPos(), + placed[i].getzPos() - placed[j].getzPos()); + assertTrue(apart > 0.1, "input slots overlap at the center"); + } + } + } + + @Test + void soupLiquidAndHiddenFoodStayCentered() { + assertNull(BowlIngredientLayout.offsetFor("liquid")); + assertNull(BowlIngredientLayout.offsetFor("food_item")); + assertNull(BowlIngredientLayout.offsetFor("input_6")); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/utils/SlotSnapshotTest.java b/src/test/java/net/tfminecraft/cooking/utils/SlotSnapshotTest.java new file mode 100644 index 0000000..03808b8 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/utils/SlotSnapshotTest.java @@ -0,0 +1,45 @@ +package net.tfminecraft.cooking.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class SlotSnapshotTest { + + @Test + void snapshotKeepsEachIngredientModel() { + String encoded = Encoder.formatSlot("input_1", "APPLE", 0, "tfmc_cooking:red_meat_steak_boiled") + + ":" + + Encoder.formatSlot("input_2", "APPLE", 0, "tfmc_cooking:pork_steak_boiled"); + + String[] parts = encoded.split(":", -1); + assertEquals(2, parts.length); + Encoder.ParsedSlot first = Encoder.parseSlot(parts[0]); + Encoder.ParsedSlot second = Encoder.parseSlot(parts[1]); + + assertEquals("input_1", first.slotId()); + assertEquals("tfmc_cooking:red_meat_steak_boiled", first.itemModel()); + assertEquals("input_2", second.slotId()); + assertEquals("tfmc_cooking:pork_steak_boiled", second.itemModel()); + } + + @Test + void snapshotKeepsItemModelKeysThatContainDots() { + Encoder.ParsedSlot parsed = Encoder.parseSlot( + Encoder.formatSlot("input_3", "CARROT", 4, "tfmc_cooking:item/carrot.boiled")); + + assertEquals("CARROT", parsed.material()); + assertEquals(4, parsed.customModelData()); + assertEquals("tfmc_cooking:item/carrot.boiled", parsed.itemModel()); + } + + @Test + void olderSnapshotsWithoutAnItemModelStillParse() { + Encoder.ParsedSlot parsed = Encoder.parseSlot("input_1.POTATO.0"); + + assertEquals("POTATO", parsed.material()); + assertEquals(0, parsed.customModelData()); + assertNull(parsed.itemModel()); + } +}