9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-19 15:09:15 +00:00

新增merchant_trade函数

This commit is contained in:
XiaoMoMi
2025-10-17 20:32:32 +08:00
parent 700a95f984
commit 5356427440
13 changed files with 222 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
package net.momirealms.craftengine.bukkit.util;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryEvent;
@@ -74,6 +75,11 @@ public final class LegacyInventoryUtils {
player.openMerchant(merchant, true);
}
@SuppressWarnings("deprecation")
public static Merchant createMerchant() {
return Bukkit.createMerchant("Villager");
}
public static Player getPlayerFromInventoryEvent(InventoryEvent event) {
return (Player) event.getView().getPlayer();
}

View File

@@ -26,6 +26,7 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantRecipe;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -91,26 +92,4 @@ public class BukkitPlatform implements Platform {
}
return new BukkitParticleType(particle, name);
}
@Override
public void openMerchant(Player player, Component title, List<MerchantOffer<?>> offers) {
Merchant merchant = Bukkit.createMerchant();
List<MerchantRecipe> recipes = merchant.getRecipes();
for (MerchantOffer<?> offer : offers) {
MerchantRecipe merchantRecipe = new MerchantRecipe((ItemStack) offer.result().getItem(), 0, Integer.MAX_VALUE, offer.xp() > 0, offer.xp(), 0);
merchantRecipe.addIngredient((ItemStack) offer.cost1().getItem());
offer.cost2().ifPresent(it -> merchantRecipe.addIngredient((ItemStack) it.getItem()));
recipes.add(merchantRecipe);
}
merchant.setRecipes(recipes);
if (title != null) {
try {
Object minecraftMerchant = CraftBukkitReflections.method$CraftMerchant$getMerchant.invoke(merchant);
CraftBukkitReflections.field$MinecraftMerchant$title.set(minecraftMerchant, ComponentUtils.adventureToMinecraft(title));
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to update merchant title", e);
}
}
LegacyInventoryUtils.openMerchant((org.bukkit.entity.Player) player.platformPlayer(), merchant);
}
}

View File

@@ -1,29 +1,44 @@
package net.momirealms.craftengine.bukkit.plugin.gui;
import io.papermc.paper.event.player.PlayerPurchaseEvent;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.block.entity.BlockEntityHolder;
import net.momirealms.craftengine.bukkit.block.entity.SimpleStorageBlockEntity;
import net.momirealms.craftengine.bukkit.nms.FastNMS;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.reflection.bukkit.CraftBukkitReflections;
import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections;
import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.NetworkReflections;
import net.momirealms.craftengine.bukkit.util.ComponentUtils;
import net.momirealms.craftengine.bukkit.util.EntityUtils;
import net.momirealms.craftengine.bukkit.util.InventoryUtils;
import net.momirealms.craftengine.bukkit.util.LegacyInventoryUtils;
import net.momirealms.craftengine.core.item.trade.MerchantOffer;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.gui.*;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Bukkit;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantRecipe;
import java.util.ArrayList;
import java.util.List;
public class BukkitGuiManager implements GuiManager, Listener {
public static final int CRAFT_ENGINE_MAGIC_MERCHANT_NUMBER = 1821981731;
private static BukkitGuiManager instance;
private final BukkitCraftEngine plugin;
@@ -135,6 +150,43 @@ public class BukkitGuiManager implements GuiManager, Listener {
}
}
// 为了修复没有经验的问题
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onMerchantTrade(PlayerPurchaseEvent event) {
MerchantRecipe trade = event.getTrade();
if (trade.getMaxUses() == CRAFT_ENGINE_MAGIC_MERCHANT_NUMBER) {
Player player = event.getPlayer();
int exp = trade.getVillagerExperience();
if (exp <= 0) return;
EntityUtils.spawnEntity(player.getWorld(), player.getLocation(), EntityType.EXPERIENCE_ORB, entity -> {
ExperienceOrb orb = (ExperienceOrb) entity;
orb.setExperience(exp);
});
}
}
@Override
public void openMerchant(net.momirealms.craftengine.core.entity.player.Player player, Component title, List<MerchantOffer<?>> offers) {
Merchant merchant = VersionHelper.isOrAbove1_21_4() ? Bukkit.createMerchant() : LegacyInventoryUtils.createMerchant();
List<MerchantRecipe> recipes = new ArrayList<>();
for (MerchantOffer<?> offer : offers) {
MerchantRecipe merchantRecipe = new MerchantRecipe((ItemStack) offer.result().getItem(), 0, CRAFT_ENGINE_MAGIC_MERCHANT_NUMBER, false, offer.xp(), 0);
merchantRecipe.addIngredient((ItemStack) offer.cost1().getItem());
offer.cost2().ifPresent(it -> merchantRecipe.addIngredient((ItemStack) it.getItem()));
recipes.add(merchantRecipe);
}
merchant.setRecipes(recipes);
if (title != null) {
try {
Object minecraftMerchant = CraftBukkitReflections.method$CraftMerchant$getMerchant.invoke(merchant);
CraftBukkitReflections.field$MinecraftMerchant$title.set(minecraftMerchant, ComponentUtils.adventureToMinecraft(title));
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to update merchant title", e);
}
}
LegacyInventoryUtils.openMerchant((org.bukkit.entity.Player) player.platformPlayer(), merchant);
}
public static BukkitGuiManager instance() {
return instance;
}

View File

@@ -466,6 +466,9 @@ warning.config.function.set_variable.missing_value: "<yellow>Issue found in file
warning.config.function.toast.missing_toast: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'toast' argument for 'toast' function.</yellow>"
warning.config.function.toast.missing_icon: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'icon' argument for 'toast' function.</yellow>"
warning.config.function.toast.invalid_advancement_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is using an invalid advancement type '<arg:2>' for 'toast' function. Allowed types: [<arg:3>].</yellow>"
warning.config.function.merchant_trade.missing_offers: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'offers' argument for 'merchant_trade' function.</yellow>"
warning.config.function.merchant_trade.offer.missing_cost_1: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'cost-1' argument for merchant trade offers.</yellow>"
warning.config.function.merchant_trade.offer.missing_result: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'result' argument for merchant trade offers.</yellow>"
warning.config.selector.missing_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'type' argument for selector.</yellow>"
warning.config.selector.invalid_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is using an invalid selector type '<arg:2>'.</yellow>"
warning.config.selector.invalid_target: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is using an invalid selector target '<arg:2>'.</yellow>"

View File

@@ -491,13 +491,24 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
}
ExceptionCollector<LocalizedResourceConfigException> eCollector1 = new ExceptionCollector<>();
Map<EventTrigger, List<Function<PlayerOptionalContext>>> events;
try {
events = EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event"));
} catch (LocalizedResourceConfigException e) {
eCollector1.add(e);
events = Map.of();
}
LootTable<?> lootTable;
try {
lootTable = LootTable.fromMap(ResourceConfigUtils.getAsMapOrNull(section.get("loot"), "loot"));
} catch (LocalizedResourceConfigException e) {
eCollector1.add(e);
lootTable = null;
}
// 创建自定义方块
AbstractCustomBlock customBlock = (AbstractCustomBlock) createCustomBlock(
holder,
variantProvider,
EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event")),
LootTable.fromMap(ResourceConfigUtils.getAsMapOrNull(section.get("loot"), "loot"))
);
AbstractCustomBlock customBlock = (AbstractCustomBlock) createCustomBlock(holder, variantProvider, events, lootTable);
BlockBehavior blockBehavior = createBlockBehavior(customBlock, MiscUtils.getAsMapList(ResourceConfigUtils.get(section, "behavior", "behaviors")));
Map<String, Map<String, Object>> appearanceConfigs;
@@ -592,7 +603,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
// 至少有一个外观吧
Objects.requireNonNull(anyAppearance, "any appearance should not be null");
ExceptionCollector<LocalizedResourceConfigException> exceptionCollector = new ExceptionCollector<>();
ExceptionCollector<LocalizedResourceConfigException> eCollector2 = new ExceptionCollector<>();
if (!singleState) {
Map<String, Object> variantsSection = ResourceConfigUtils.getAsMapOrNull(stateSection.get("variants"), "variants");
if (variantsSection != null) {
@@ -602,7 +613,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
// 先解析nbt找到需要修改的方块状态
CompoundTag tag = BlockNbtParser.deserialize(variantProvider, variantNBT);
if (tag == null) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", variantNBT));
eCollector2.add(new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", variantNBT));
continue;
}
List<ImmutableBlockState> possibleStates = variantProvider.getPossibleStates(tag);
@@ -612,11 +623,11 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
possibleState.setSettings(BlockSettings.ofFullCopy(possibleState.settings(), anotherSetting));
}
}
String appearanceName = ResourceConfigUtils.getAsString(variantSection.get("appearance"));
String appearanceName = ResourceConfigUtils.getAsStringOrNull(variantSection.get("appearance"));
if (appearanceName != null) {
BlockStateAppearance appearance = appearances.get(appearanceName);
if (appearance == null) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", variantNBT, appearanceName));
eCollector2.add(new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", variantNBT, appearanceName));
continue;
}
for (ImmutableBlockState possibleState : possibleStates) {
@@ -675,8 +686,11 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
AbstractBlockManager.this.byId.put(customBlock.id(), customBlock);
// 抛出次要警告
exceptionCollector.throwIfPresent();
eCollector2.throwIfPresent();
}, () -> GsonHelper.get().toJson(section)));
// 抛出次要警告
eCollector1.throwIfPresent();
}, () -> GsonHelper.get().toJson(section)));
}

View File

@@ -26,6 +26,4 @@ public interface Platform {
World getWorld(String name);
ParticleType getParticleType(Key name);
void openMerchant(Player player, Component title, List<MerchantOffer<?>> offers);
}

View File

@@ -46,6 +46,7 @@ public class EventFunctions {
register(CommonFunctions.SET_VARIABLE, new SetVariableFunction.FactoryImpl<>(EventConditions::fromMap));
register(CommonFunctions.TOAST, new ToastFunction.FactoryImpl<>(EventConditions::fromMap));
register(CommonFunctions.DAMAGE, new DamageFunction.FactoryImpl<>(EventConditions::fromMap));
register(CommonFunctions.MERCHANT_TRADE, new MerchantTradeFunction.FactoryImpl<>(EventConditions::fromMap));
}
public static void register(Key key, FunctionFactory<PlayerOptionalContext> factory) {

View File

@@ -35,4 +35,5 @@ public final class CommonFunctions {
public static final Key TOAST = Key.of("craftengine:toast");
public static final Key SET_VARIABLE = Key.of("craftengine:set_variable");
public static final Key DAMAGE = Key.of("craftengine:damage");
public static final Key MERCHANT_TRADE = Key.of("craftengine:merchant_trade");
}

View File

@@ -43,7 +43,7 @@ public class DamageFunction<CTX extends Context> extends AbstractConditionalFunc
@Override
public Function<CTX> create(Map<String, Object> arguments) {
PlayerSelector<CTX> selector = PlayerSelectors.fromObject(arguments.getOrDefault("target", "self"), conditionFactory());
Key damageType = Key.of(ResourceConfigUtils.getAsString(arguments.getOrDefault("damage-type", "generic")));
Key damageType = Key.of(ResourceConfigUtils.getAsStringOrNull(arguments.getOrDefault("damage-type", "generic")));
NumberProvider amount = NumberProviders.fromObject(arguments.getOrDefault("amount", 1f));
return new DamageFunction<>(selector, damageType, amount, getPredicates(arguments));
}

View File

@@ -0,0 +1,122 @@
package net.momirealms.craftengine.core.plugin.context.function;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.ItemKeys;
import net.momirealms.craftengine.core.item.modifier.ComponentsModifier;
import net.momirealms.craftengine.core.item.modifier.TagsModifier;
import net.momirealms.craftengine.core.item.trade.MerchantOffer;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.context.*;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.context.selector.PlayerSelector;
import net.momirealms.craftengine.core.plugin.context.selector.PlayerSelectors;
import net.momirealms.craftengine.core.util.*;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MerchantTradeFunction<CTX extends Context> extends AbstractConditionalFunction<CTX> {
private final String title;
private final PlayerSelector<CTX> selector;
private final LazyReference<List<MerchantOffer<?>>> offers;
public MerchantTradeFunction(List<Condition<CTX>> predicates, @Nullable PlayerSelector<CTX> selector, String title, LazyReference<List<MerchantOffer<?>>> offers) {
super(predicates);
this.title = title;
this.selector = selector;
this.offers = offers;
}
@Override
public void runInternal(CTX ctx) {
if (this.selector == null) {
ctx.getOptionalParameter(DirectContextParameters.PLAYER).ifPresent(it -> {
CraftEngine.instance().guiManager().openMerchant(it, this.title == null ? null : AdventureHelper.miniMessage().deserialize(this.title, ctx.tagResolvers()), this.offers.get());
});
} else {
for (Player viewer : this.selector.get(ctx)) {
RelationalContext relationalContext = ViewerContext.of(ctx, PlayerOptionalContext.of(viewer));
CraftEngine.instance().guiManager().openMerchant(viewer, this.title == null ? null : AdventureHelper.miniMessage().deserialize(this.title, relationalContext.tagResolvers()), this.offers.get());
}
}
}
@Override
public Key type() {
return CommonFunctions.MESSAGE;
}
public static class FactoryImpl<CTX extends Context> extends AbstractFactory<CTX> {
public FactoryImpl(java.util.function.Function<Map<String, Object>, Condition<CTX>> factory) {
super(factory);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Function<CTX> create(Map<String, Object> arguments) {
String title = ResourceConfigUtils.getAsStringOrNull(arguments.get("title"));
List<TempOffer> merchantOffers = ResourceConfigUtils.parseConfigAsList(ResourceConfigUtils.requireNonNullOrThrow(arguments.get("offers"), "warning.config.function.merchant_trade.missing_offers"), map -> {
Object cost1 = ResourceConfigUtils.requireNonNullOrThrow(map.get("cost-1"), "warning.config.function.merchant_trade.offer.missing_cost_1");
Object cost2 = map.get("cost-2");
Object result = ResourceConfigUtils.requireNonNullOrThrow(map.get("result"), "warning.config.function.merchant_trade.offer.missing_result");
int exp = ResourceConfigUtils.getAsInt(map.get("experience"), "experience");
return new TempOffer(cost1, cost2, result, exp);
});
return new MerchantTradeFunction<>(getPredicates(arguments), PlayerSelectors.fromObject(arguments.get("target"), conditionFactory()), title,
LazyReference.lazyReference(() -> {
List<MerchantOffer<?>> offers = new ArrayList<>(merchantOffers.size());
for (TempOffer offer : merchantOffers) {
Item cost1 = parseIngredient(offer.cost1);
Optional cost2 = Optional.ofNullable(parseIngredient(offer.cost2));
Item result = parseIngredient(offer.result);
offers.add(new MerchantOffer<>(cost1, cost2, result, false, 0, Integer.MAX_VALUE, offer.exp, 0, 0, 0));
}
return offers;
}));
}
public record TempOffer(Object cost1, Object cost2, Object result, int exp) {
}
private Item<?> parseIngredient(Object arguments) {
if (arguments == null) return null;
if (arguments instanceof Map<?,?> map) {
Map<String, Object> args = MiscUtils.castToMap(map, false);
String itemName = args.getOrDefault("item", "minecraft:stone").toString();
Item<Object> item = createSafeItem(itemName);
if (args.containsKey("count")) {
item.count(ResourceConfigUtils.getAsInt(args.get("count"), "count"));
}
if (VersionHelper.isOrAbove1_20_5() && args.containsKey("components")) {
item = new ComponentsModifier<>(MiscUtils.castToMap(args.get("components"), false)).apply(item, ItemBuildContext.empty());
}
if (!VersionHelper.isOrAbove1_20_5() && args.containsKey("nbt")) {
item = new TagsModifier<>(MiscUtils.castToMap(args.get("nbt"), false)).apply(item, ItemBuildContext.empty());
}
return item;
} else {
String itemName = arguments.toString();
return createSafeItem(itemName);
}
}
private Item<Object> createSafeItem(String itemName) {
Key itemId = Key.of(itemName);
Item<Object> item = CraftEngine.instance().itemManager().createWrappedItem(itemId, null);
if (item == null) {
item = CraftEngine.instance().itemManager().createWrappedItem(ItemKeys.STONE, null);
assert item != null;
item.itemNameComponent(Component.text(itemName).color(NamedTextColor.RED));
}
return item;
}
}
}

View File

@@ -2,8 +2,11 @@ package net.momirealms.craftengine.core.plugin.gui;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.trade.MerchantOffer;
import net.momirealms.craftengine.core.plugin.Manageable;
import java.util.List;
public interface GuiManager extends Manageable {
void openInventory(Player player, GuiType guiType);
@@ -11,4 +14,6 @@ public interface GuiManager extends Manageable {
void updateInventoryTitle(Player player, Component component);
Inventory createInventory(Gui gui, int size);
void openMerchant(Player player, Component title, List<MerchantOffer<?>> offers);
}

View File

@@ -1,11 +1,13 @@
package net.momirealms.craftengine.core.plugin.locale;
import aQute.bnd.annotation.jpms.Open;
import net.momirealms.craftengine.core.util.AdventureHelper;
import net.momirealms.craftengine.core.util.ArrayUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Optional;
public class LocalizedException extends RuntimeException {
private final String node;
@@ -72,8 +74,8 @@ public class LocalizedException extends RuntimeException {
private String generateLocalizedMessage() {
try {
String rawMessage = TranslationManager.instance()
.miniMessageTranslation(this.node);
String rawMessage = Optional.ofNullable(TranslationManager.instance()
.miniMessageTranslation(this.node)).orElse(this.node);
String cleanMessage = AdventureHelper.miniMessage()
.stripTags(rawMessage);
for (int i = 0; i < arguments.length; i++) {

View File

@@ -23,7 +23,7 @@ public final class ResourceConfigUtils {
return raw != null ? function.apply(raw) : defaultValue;
}
public static String getAsString(@Nullable Object raw) {
public static String getAsStringOrNull(@Nullable Object raw) {
if (raw == null) {
return null;
}