From 1ac69bc83aa05eb38760af09ef60742c6601f13a Mon Sep 17 00:00:00 2001 From: MATRIX-feather Date: Tue, 8 Oct 2024 17:18:03 +0800 Subject: [PATCH] add: recipe! --- src/main/java/xiamomc/morph/MorphPlugin.java | 3 + .../subcommands/plugin/ReloadSubCommand.java | 6 + .../xiamomc/morph/config/ConfigOption.java | 2 + .../morph/events/CommonEventProcessor.java | 62 ++-- .../morph/misc/recipe/RecipeManager.java | 274 ++++++++++++++++++ .../morph/misc/recipe/RecipeOptions.java | 29 ++ .../morph/misc/recipe/RecipeProperty.java | 19 ++ .../recipe/StandaloneYamlConfigManager.java | 116 ++++++++ src/main/resources/recipes.yml | 38 +++ 9 files changed, 515 insertions(+), 34 deletions(-) create mode 100644 src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java create mode 100644 src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java create mode 100644 src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java create mode 100644 src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java create mode 100644 src/main/resources/recipes.yml diff --git a/src/main/java/xiamomc/morph/MorphPlugin.java b/src/main/java/xiamomc/morph/MorphPlugin.java index 7f2993eb..dc5183e8 100644 --- a/src/main/java/xiamomc/morph/MorphPlugin.java +++ b/src/main/java/xiamomc/morph/MorphPlugin.java @@ -19,6 +19,7 @@ import xiamomc.morph.messages.vanilla.VanillaMessageStore; import xiamomc.morph.misc.NetworkingHelper; import xiamomc.morph.misc.PlayerOperationSimulator; +import xiamomc.morph.misc.recipe.RecipeManager; import xiamomc.morph.misc.disguiseProperty.DisguiseProperties; import xiamomc.morph.misc.gui.IconLookup; import xiamomc.morph.misc.integrations.modelengine.ModelEngineHelper; @@ -231,6 +232,8 @@ public void onEnable() dependencyManager.cache(DisguiseProperties.INSTANCE); + dependencyManager.cache(new RecipeManager()); + mirrorProcessor = new InteractionMirrorProcessor(); //注册EventProcessor diff --git a/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java b/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java index 16bd3443..5258df58 100644 --- a/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java +++ b/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java @@ -12,6 +12,7 @@ import xiamomc.morph.messages.MessageUtils; import xiamomc.morph.messages.MorphMessageStore; import xiamomc.morph.messages.vanilla.VanillaMessageStore; +import xiamomc.morph.misc.recipe.RecipeManager; import xiamomc.morph.misc.skins.PlayerSkinProvider; import xiamomc.morph.network.multiInstance.MultiInstanceService; import xiamomc.morph.network.server.MorphClientHandler; @@ -62,6 +63,9 @@ public FormattableMessage getHelpMessage() @Resolved private MultiInstanceService multiInstanceService; + @Resolved + private RecipeManager recipeManager; + private final List subcommands = ObjectImmutableList.of("data", "message", "update_message"); @Override @@ -103,6 +107,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull String[] args) PlayerSkinProvider.getInstance().reload(); multiInstanceService.onReload(); + + recipeManager.reload(); } if (reloadsMessage) diff --git a/src/main/java/xiamomc/morph/config/ConfigOption.java b/src/main/java/xiamomc/morph/config/ConfigOption.java index 586cc1ff..43778af6 100644 --- a/src/main/java/xiamomc/morph/config/ConfigOption.java +++ b/src/main/java/xiamomc/morph/config/ConfigOption.java @@ -7,6 +7,7 @@ import xiamomc.pluginbase.Configuration.ConfigNode; import java.util.ArrayList; +import java.util.HashMap; public enum ConfigOption { @@ -124,6 +125,7 @@ public enum ConfigOption // SRR -> ServerRenderer SR_SHOW_PLAYER_DISGUISES_IN_TAB(serverRendererNode().append("show_player_disguises_in_tab"), false), + VERSION(ConfigNode.create().append("version"), 0); public final ConfigNode node; diff --git a/src/main/java/xiamomc/morph/events/CommonEventProcessor.java b/src/main/java/xiamomc/morph/events/CommonEventProcessor.java index 8ea04481..82414371 100644 --- a/src/main/java/xiamomc/morph/events/CommonEventProcessor.java +++ b/src/main/java/xiamomc/morph/events/CommonEventProcessor.java @@ -3,8 +3,6 @@ import com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent; import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.kyori.adventure.key.Key; -import net.kyori.adventure.sound.Sound; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.attribute.Attribute; @@ -16,10 +14,7 @@ import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.entity.*; import org.bukkit.event.player.*; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.checkerframework.checker.units.qual.A; +import org.bukkit.inventory.*; import xiamomc.morph.MorphManager; import xiamomc.morph.MorphPluginObject; import xiamomc.morph.RevealingHandler; @@ -30,7 +25,6 @@ import xiamomc.morph.messages.HintStrings; import xiamomc.morph.messages.MessageUtils; import xiamomc.morph.messages.MorphStrings; -import xiamomc.morph.messages.SkillStrings; import xiamomc.morph.messages.vanilla.VanillaMessageStore; import xiamomc.morph.misc.DisguiseTypes; import xiamomc.morph.misc.OfflineDisguiseResult; @@ -73,6 +67,33 @@ public class CommonEventProcessor extends MorphPluginObject implements Listener private Bindable unMorphOnDeath; + private final Bindable doRevealing = new Bindable<>(true); + + private final Bindable allowAcquireMorphs = new Bindable<>(false); + + @Initializer + private void load() + { + config.bind(cooldownOnDamage, ConfigOption.SKILL_COOLDOWN_ON_DAMAGE); + config.bind(bruteIgnoreDisguises, ConfigOption.PIGLIN_BRUTE_IGNORE_DISGUISES); + config.bind(doRevealing, ConfigOption.REVEALING); + config.bind(allowAcquireMorphs, ConfigOption.ALLOW_ACQUIRE_MORPHS); + + unMorphOnDeath = config.getBindable(Boolean.class, ConfigOption.UNMORPH_ON_DEATH); + this.addSchedule(this::update); + } + + private void update() + { + this.addSchedule(this::update); + + if (plugin.getCurrentTick() % 8 == 0) + { + playersMinedGoldBlocks.clear(); + susIncreasedPlayers.clear(); + } + } + @EventHandler public void onEntityDeath(EntityDeathEvent e) { @@ -143,33 +164,6 @@ public void onPlayerTookDamage(EntityDamageEvent e) } } - private final Bindable doRevealing = new Bindable<>(true); - - private final Bindable allowAcquireMorphs = new Bindable<>(false); - - @Initializer - private void load() - { - config.bind(cooldownOnDamage, ConfigOption.SKILL_COOLDOWN_ON_DAMAGE); - config.bind(bruteIgnoreDisguises, ConfigOption.PIGLIN_BRUTE_IGNORE_DISGUISES); - config.bind(doRevealing, ConfigOption.REVEALING); - config.bind(allowAcquireMorphs, ConfigOption.ALLOW_ACQUIRE_MORPHS); - - unMorphOnDeath = config.getBindable(Boolean.class, ConfigOption.UNMORPH_ON_DEATH); - this.addSchedule(this::update); - } - - private void update() - { - this.addSchedule(this::update); - - if (plugin.getCurrentTick() % 8 == 0) - { - playersMinedGoldBlocks.clear(); - susIncreasedPlayers.clear(); - } - } - @EventHandler public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent e) { diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java new file mode 100644 index 00000000..449e4a54 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java @@ -0,0 +1,274 @@ +package xiamomc.morph.misc.recipe; + +import com.google.common.base.Charsets; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xiamomc.morph.MorphPluginObject; +import xiamomc.morph.config.MorphConfigManager; +import xiamomc.morph.utilities.ItemUtils; +import xiamomc.morph.utilities.PluginAssetUtils; +import xiamomc.pluginbase.Annotations.Initializer; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class RecipeManager extends MorphPluginObject +{ + @Nullable + private YamlConfiguration yamlConfiguration; + + private final File configFile = new File(plugin.getDataFolder(), "recipes.yml"); + + private boolean allowCrafting = false; + private boolean unShaped = false; + private List shape = new ObjectArrayList<>(); + private Map materials = new Object2ObjectOpenHashMap<>(); + private String resultMaterialId = "~UNSET"; + private String resultName = "~UNSET"; + private List resultLore = new ObjectArrayList<>(); + + public void reload() + { + var newConfig = new YamlConfiguration(); + + if (!configFile.exists()) + { + if (!copyInternalRecipeResource()) + { + logger.error("Can't create file to save configuration! Not reloading recipes..."); + return; + } + } + + try + { + newConfig.load(configFile); + } + catch (Throwable e) + { + logger.error("Unable to load recipe configuration: " + e.getMessage()); + return; + } + + this.yamlConfiguration = newConfig; + readValuesFromConfig(newConfig); + prepareRecipe(); + } + + private void readValuesFromConfig(YamlConfiguration config) + { + allowCrafting = config.getBoolean(RecipeOptions.ALLOW_SKILL_ITEM_CRAFTING.toString(), false); + unShaped = config.getBoolean(RecipeOptions.SKILL_ITEM_CRAFTING_UNSHAPED.toString(), false); + shape = config.getStringList(RecipeOptions.SKILL_ITEM_CRAFTING_SHAPE.toString()); + resultMaterialId = config.getString(RecipeOptions.SKILL_ITEM_RESULT_MATERIAL.toString(), "~UNSET"); + resultName = config.getString(RecipeOptions.SKILL_ITEM_RESULT_NAME.toString(), "~UNSET"); + resultLore = config.getStringList(RecipeOptions.SKILL_ITEM_RESULT_LORE.toString()); + + var materialSection = config.getConfigurationSection(RecipeOptions.SKILL_ITEM_CRAFTING_MATERIALS.toString()); + + if (materialSection != null) + { + materialSection.getKeys(false).forEach(key -> + { + var value = materialSection.getString(key, "~UNSET"); + materials.put(key, value); + }); + } + } + + private boolean copyInternalRecipeResource() + { + try + { + if (!configFile.createNewFile()) + return false; + + try (var writer = new OutputStreamWriter(new FileOutputStream(configFile), Charsets.UTF_8)) + { + writer.write(PluginAssetUtils.getFileStrings("recipes.yml")); + } + catch (Throwable t) + { + logger.error("Can't write content: " + t.getMessage()); + return false; + } + + return true; + } + catch (Throwable t) + { + logger.error("Can't create config file: " + t.getMessage()); + } + + return true; + } + + @Initializer + private void load(MorphConfigManager configManager) + { + if (this.yamlConfiguration == null) + reload(); + } + + @Nullable + private Material getMaterialFrom(String str) + { + var key = NamespacedKey.fromString(str); + + return Arrays.stream(Material.values()).parallel().filter(m -> m.key().equals(key)) + .findFirst().orElse(null); + } + + @NotNull + public static final NamespacedKey SKILLITEM_CRAFTING_KEY = NamespacedKey.fromString("feathermorph:skill_item_crafting"); + + private void prepareRecipe() + { + if (!allowCrafting) + { + Bukkit.removeRecipe(SKILLITEM_CRAFTING_KEY); + return; + } + + var minimessage = MiniMessage.miniMessage(); + + Component name = this.resultName.equals("~UNSET") ? null : minimessage.deserialize(this.resultName); + List loreComponents = this.resultLore.isEmpty() ? null : this.resultLore.parallelStream().map(minimessage::deserialize).toList(); + + var resultMaterial = this.getMaterialFrom(this.resultMaterialId); + if (resultMaterial == null) + { + logger.error("Invalid result material ID: '%s', skipping...".formatted(resultMaterialId)); + return; + } + + Map materialsReal = new Object2ObjectOpenHashMap<>(); + this.materials.forEach((str, id) -> + { + var material = Arrays.stream(Material.values()) + .filter(m -> m.key().equals(NamespacedKey.fromString(id))) + .findFirst() + .orElse(null); + + if (material == null) + { + logger.warn("Invalid material '%s', skipping...".formatted(id)); + return; + } + + materialsReal.put(str, material); + }); + + var recipeProperty = new RecipeProperty(SKILLITEM_CRAFTING_KEY, + !this.unShaped, this.shape, + materialsReal, + resultMaterial, + name, + loreComponents); + + buildAndAddRecipe(recipeProperty); + } + + private void buildAndAddRecipe(RecipeProperty recipeProperty) + { + var resultItem = ItemUtils.buildSkillItemFrom(ItemStack.of(recipeProperty.resultMaterial())); + resultItem.editMeta(meta -> + { + meta.setRarity(ItemRarity.UNCOMMON); + meta.setEnchantmentGlintOverride(true); + + var name = recipeProperty.resultName(); + if (name != null) + meta.itemName(name); + + var lore = recipeProperty.lore(); + if (lore != null && !lore.isEmpty()) + meta.lore(lore); + }); + + var key = recipeProperty.key(); + + CraftingRecipe recipe; + + if (recipeProperty.shaped()) + { + var shaped = new ShapedRecipe(key, resultItem); + shaped.shape(recipeProperty.shape().toArray(new String[]{})); + recipeProperty.materials().forEach((ch, material) -> shaped.setIngredient(ch.charAt(0), material)); + + recipe = shaped; + } + else + { + var shapeless = new ShapelessRecipe(key, resultItem); + recipeProperty.materials().forEach((ignored, material) -> shapeless.addIngredient(material)); + + recipe = shapeless; + } + + Bukkit.removeRecipe(key); + Bukkit.addRecipe(recipe); + } + + private void test_dumpExsampleConfig() + { + allowCrafting = true; + unShaped = true; + shape = List.of( + "ABC", + "DEF", + "GHI" + ); + resultMaterialId = Material.BEDROCK.key().asString(); + resultName = "技能物品"; + resultLore = List.of( + "技能测试1", "技能测试2" + ); + materials = new Object2ObjectOpenHashMap<>(); + materials.put("A", Material.BEDROCK.key().asString()); + materials.put("B", Material.ACACIA_BOAT.key().asString()); + } + + private void test_saveConfig() + { + if (yamlConfiguration == null) + { + logger.error("Null config!"); + return; + } + + yamlConfiguration.set(RecipeOptions.ALLOW_SKILL_ITEM_CRAFTING.toString(), allowCrafting); + yamlConfiguration.set(RecipeOptions.SKILL_ITEM_CRAFTING_UNSHAPED.toString(), this.unShaped); + yamlConfiguration.set(RecipeOptions.SKILL_ITEM_CRAFTING_SHAPE.toString(), this.shape); + yamlConfiguration.set(RecipeOptions.SKILL_ITEM_RESULT_MATERIAL.toString(), this.resultMaterialId); + yamlConfiguration.set(RecipeOptions.SKILL_ITEM_RESULT_NAME.toString(), this.resultName); + yamlConfiguration.set(RecipeOptions.SKILL_ITEM_RESULT_LORE.toString(), this.resultLore); + + this.materials.forEach((str, id) -> + { + var node = RecipeOptions.SKILL_ITEM_CRAFTING_MATERIALS.toString() + "." + str; + yamlConfiguration.set(node, id); + }); + + try + { + yamlConfiguration.save(configFile); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java new file mode 100644 index 00000000..360c6e3a --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java @@ -0,0 +1,29 @@ +package xiamomc.morph.misc.recipe; + +import xiamomc.pluginbase.Configuration.ConfigNode; +import xiamomc.pluginbase.Configuration.ConfigOption; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RecipeOptions +{ + public static final ConfigOption ALLOW_SKILL_ITEM_CRAFTING = new ConfigOption<>(skillItemNode().append("enabled"), true); + public static final ConfigOption SKILL_ITEM_CRAFTING_UNSHAPED = new ConfigOption<>(skillItemNode().append("shapeless"), true); + public static final ConfigOption> SKILL_ITEM_CRAFTING_SHAPE = new ConfigOption<>(skillItemNode().append("crafting_shape"), new ArrayList<>()); + public static final ConfigOption> SKILL_ITEM_CRAFTING_MATERIALS = new ConfigOption<>(skillItemNode().append("crafting_materials"), new HashMap<>()); + public static final ConfigOption SKILL_ITEM_RESULT_MATERIAL = new ConfigOption<>(skillItemNode().append("result_material"), "minecraft:feather"); + public static final ConfigOption SKILL_ITEM_RESULT_NAME = new ConfigOption<>(skillItemNode().append("result_item_name"), "~UNSET"); + public static final ConfigOption> SKILL_ITEM_RESULT_LORE = new ConfigOption<>(skillItemNode().append("result_item_lore"), new ArrayList<>()); + + private static ConfigNode craftingNode() + { + return ConfigNode.create().append("item_crafting"); + } + private static ConfigNode skillItemNode() + { + return craftingNode().append("skill_item"); + } +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java new file mode 100644 index 00000000..a0a70196 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java @@ -0,0 +1,19 @@ +package xiamomc.morph.misc.recipe; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +public record RecipeProperty(NamespacedKey key, + boolean shaped, List shape, + Map materials, + @NotNull Material resultMaterial, + @Nullable Component resultName, + @Nullable List lore) +{ +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java b/src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java new file mode 100644 index 00000000..f0d1bfa5 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java @@ -0,0 +1,116 @@ +package xiamomc.morph.misc.recipe; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.bukkit.configuration.file.YamlConfiguration; +import xiamomc.morph.MorphPluginObject; +import xiamomc.pluginbase.Bindables.Bindable; +import xiamomc.pluginbase.Configuration.ConfigOption; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class StandaloneYamlConfigManager extends MorphPluginObject +{ + private YamlConfiguration backendConfiguration; + + private final File file; + + public StandaloneYamlConfigManager(File file) + { + this.file = file; + + reload(); + } + + /** + * Copy internal resource to the location + * @return Whether this operation was successful + */ + protected abstract boolean copyInternalResource(); + + public void reload() + { + var newConfig = new YamlConfiguration(); + + if (!file.exists()) + { + if (!copyInternalResource()) + { + logger.error("Can't create file to save configuration! Not reloading recipes..."); + return; + } + } + + try + { + newConfig.load(file); + } + catch (Throwable e) + { + logger.error("Unable to load recipe configuration: " + e.getMessage()); + return; + } + + this.backendConfiguration = newConfig; + } + + private final Map> bindableMap = new ConcurrentHashMap<>(); + + public Bindable getBindable(ConfigOption option) + { + var cache = bindableMap.get(option.toString()); + if (cache != null) return (Bindable) cache; + + if (Map.class.isAssignableFrom(option.getDefault().getClass())) + throw new IllegalArgumentException("Maps cannot being used with Bindable"); + + var value = this.getOrDefault(option, option.getDefault()); + + var bindable = new Bindable(value); + bindableMap.put(option.toString(), bindable); + + return bindable; + } + + public T get(ConfigOption option) + { + return getOrDefault(option, null); + } + + /** + * @apiNote List classes will ALWAYS return ArrayList + */ + public T getOrDefault(ConfigOption option, T defaultVal) + { + var node = option.toString(); + + Object backendResult = backendConfiguration.get(node, defaultVal); + if (backendResult == null && defaultVal == null) return null; + + var optionDefault = option.getDefault(); + + // 对列表单独处理 + if (List.class.isAssignableFrom(optionDefault.getClass())) + { + var elementClass = optionDefault.getClass(); + var newResult = backendConfiguration.getList(node, new ArrayList<>()); + newResult.removeIf((listVal) -> { + return !elementClass.isInstance(listVal); + }); + + return (T) newResult; + } + else + { + // ConfigOption#getDefault is always NotNull + if (optionDefault.getClass().isAssignableFrom(backendResult.getClass())) + return (T) backendResult; + else + return defaultVal; + } + } +} diff --git a/src/main/resources/recipes.yml b/src/main/resources/recipes.yml new file mode 100644 index 00000000..bc845aab --- /dev/null +++ b/src/main/resources/recipes.yml @@ -0,0 +1,38 @@ +root: + # Item crafting options + item_crafting: + # Options for the Skill Item + skill_item: + enabled: true + + # Should this recipe shapeless? + shapeless: true + + # The crafting shape for this recipe + # Doesn't work if shapeless is enabled + crafting_shape: + - ABC + - DEF + - GHI + + # The material of the crafted result + result_material: minecraft:feather + + # The name for the item + # Use "~UNSET" to keep the material's original name + # Supports MiniMessage + result_item_name: ~UNSET + + # The lore for the item + # + # Example: + # result_item_lore: + # - Lore1 + # - Supports MiniMessage! + result_item_lore: [] + + # Materials required to craft this item. + # Left side is the letter present in crafting_shape, and right side is the item ID + crafting_materials: + A: minecraft:feather + B: minecraft:redstone