diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..8409985 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,10 @@ +name: Build +on: [ push, pull_request ] + +jobs: + build: + uses: okocraft/workflows/.github/workflows/gradle.yml@v1 + with: + java-version: '21' + package-name: BoxTradeStick-Build-${{ github.run_number }} + upload-test-results: true diff --git a/build.gradle.kts b/build.gradle.kts index 78bb263..a6b207b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,14 @@ plugins { java id("io.papermc.paperweight.userdev") version "1.6.0" - id("io.github.goooler.shadow") version "8.1.7" } group = "net.okocraft.boxtradestick" version = "1.6-SNAPSHOT" +val mcVersion = "1.20.5" +val fullVersion = "${version}-mc${mcVersion}" + repositories { mavenCentral() maven(url = "https://repo.papermc.io/repository/maven-public/") @@ -14,16 +16,14 @@ repositories { } dependencies { - paperweight.paperDevBundle("1.20.5-R0.1-SNAPSHOT") - - implementation("com.github.siroshun09.configapi:configapi-yaml:4.6.4") - implementation("com.github.siroshun09.translationloader:translationloader:2.0.2") + paperweight.paperDevBundle("$mcVersion-R0.1-SNAPSHOT") compileOnly("net.okocraft.box:box-api:6.0.0-SNAPSHOT") + compileOnly("net.okocraft.box:box-gui-feature:6.0.0-SNAPSHOT") compileOnly("net.okocraft.box:box-stick-feature:6.0.0-SNAPSHOT") testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") - testRuntimeOnly("io.papermc.paper:paper-api:1.20.5-R0.1-SNAPSHOT") + testRuntimeOnly("io.papermc.paper:paper-api:$mcVersion-R0.1-SNAPSHOT") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } @@ -41,13 +41,9 @@ tasks { options.release.set(21) } - build { - dependsOn(shadowJar) - } - processResources { - filesMatching(listOf("plugin.yml", "en.yml", "ja_JP.yml")) { - expand("projectVersion" to version) + filesMatching(listOf("paper-plugin.yml")) { + expand("projectVersion" to version, "apiVersion" to mcVersion) } } @@ -55,8 +51,7 @@ tasks { useJUnitPlatform() } - shadowJar { - minimize() - relocate("com.github.siroshun09", "net.okocraft.boxtradestick.lib") + jar { + archiveFileName = "BoxTradeStick-$fullVersion.jar" } } diff --git a/src/main/java/net/okocraft/boxtradestick/BoxTradeStickPlugin.java b/src/main/java/net/okocraft/boxtradestick/BoxTradeStickPlugin.java index 3381aa8..c5060f7 100644 --- a/src/main/java/net/okocraft/boxtradestick/BoxTradeStickPlugin.java +++ b/src/main/java/net/okocraft/boxtradestick/BoxTradeStickPlugin.java @@ -1,90 +1,79 @@ package net.okocraft.boxtradestick; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Locale; -import java.util.jar.JarFile; -import java.util.logging.Level; -import net.kyori.adventure.key.Key; -import com.github.siroshun09.configapi.api.Configuration; -import com.github.siroshun09.configapi.api.util.ResourceUtils; -import com.github.siroshun09.configapi.yaml.YamlConfiguration; -import com.github.siroshun09.translationloader.ConfigurationLoader; -import com.github.siroshun09.translationloader.TranslationLoader; -import com.github.siroshun09.translationloader.directory.TranslationDirectory; +import com.github.siroshun09.messages.api.directory.DirectorySource; +import com.github.siroshun09.messages.api.directory.MessageProcessors; +import com.github.siroshun09.messages.api.source.StringMessageMap; +import com.github.siroshun09.messages.api.util.PropertiesFile; +import com.github.siroshun09.messages.minimessage.localization.MiniMessageLocalization; +import com.github.siroshun09.messages.minimessage.source.MiniMessageSource; +import net.okocraft.box.api.BoxAPI; +import net.okocraft.box.feature.stick.StickFeature; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class BoxTradeStickPlugin extends JavaPlugin { - - private final Path jarFile; - - private final TranslationDirectory translationDirectory; - - public BoxTradeStickPlugin() { - this.jarFile = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().getPath()); - - Path pluginDirectory = getDataFolder().toPath(); - - this.translationDirectory = - TranslationDirectory.newBuilder() - .setDirectory(pluginDirectory.resolve("languages")) - .setKey(Key.key("boxtradestick", "languages")) - .setDefaultLocale(Locale.ENGLISH) - .onDirectoryCreated(this::saveDefaultLanguages) - .setVersion(getPluginMeta().getVersion()) // getPluginMeta never returns null - .setTranslationLoaderCreator(this::getBundledTranslation) - .build(); - } - - private void saveDefaultLanguages(@NotNull Path directory) throws IOException { - var english = "en.yml"; - ResourceUtils.copyFromJarIfNotExists(jarFile, english, directory.resolve(english)); - - var japanese = "ja_JP.yml"; - ResourceUtils.copyFromJarIfNotExists(jarFile, japanese, directory.resolve(japanese)); - } - - private @Nullable TranslationLoader getBundledTranslation(@NotNull Locale locale) throws IOException { - var strLocale = locale.toString(); - - if (!(strLocale.equals("en") || strLocale.equals("ja_JP"))) { - return null; - } - - Configuration source; - - try (var jar = new JarFile(getFile()); - var input = ResourceUtils.getInputStreamFromJar(jar, strLocale + ".yml")) { - source = YamlConfiguration.loadFromInputStream(input); - } +import java.io.IOException; +import java.util.Locale; +import java.util.Map; - var loader = ConfigurationLoader.create(locale, source); - loader.load(); +public final class BoxTradeStickPlugin extends JavaPlugin { - return loader; - } + private MiniMessageLocalization localization; @Override public void onLoad() { - try { - translationDirectory.load(); + this.loadMessages(); } catch (IOException e) { - getLogger().log(Level.SEVERE, "Could not load languages", e); + this.getSLF4JLogger().error("Could not load languages.", e); } } @Override public void onEnable() { - getServer().getPluginManager().registerEvents(new PlayerListener(), this); + if (!BoxAPI.isLoaded()) { + this.getSLF4JLogger().error("Box is not loaded. All features of BoxTradeStick will not be working."); + return; + } + + BoxAPI.api().getFeatureProvider().getFeature(StickFeature.class) + .map(StickFeature::getBoxStickItem) + .map(stick -> new PlayerListener(stick, this.localization)) + .ifPresentOrElse( + listener -> this.getServer().getPluginManager().registerEvents(listener, this), + () -> this.getSLF4JLogger().error("Failed to get the Box Stick from Box.") + ); } @Override public void onDisable() { - translationDirectory.unload(); + this.getServer().getOnlinePlayers().forEach(player -> { + if (MerchantRecipesGUI.fromInventory(player.getOpenInventory().getTopInventory()) != null) { + player.closeInventory(); + } + }); } + private void loadMessages() throws IOException { + if (this.localization == null) { // on startup + this.localization = new MiniMessageLocalization(MiniMessageSource.create(StringMessageMap.create(Languages.defaultMessages())), Languages::getLocaleFrom); + } else { // on reload + this.localization.clearSources(); + } + + DirectorySource.propertiesFiles(this.getDataFolder().toPath().resolve("languages")) + .defaultLocale(Locale.ENGLISH, Locale.JAPANESE) + .messageProcessor(MessageProcessors.appendMissingMessagesToPropertiesFile(this::loadDefaultMessageMap)) + .load(loaded -> this.localization.addSource(loaded.locale(), MiniMessageSource.create(loaded.messageSource()))); + } + + private @Nullable Map loadDefaultMessageMap(@NotNull Locale locale) throws IOException { + if (locale.equals(Locale.ENGLISH)) { + return Languages.defaultMessages(); + } else { + try (var input = this.getResource(locale + ".properties")) { + return input != null ? PropertiesFile.load(input) : null; + } + } + } } diff --git a/src/main/java/net/okocraft/boxtradestick/BoxUtil.java b/src/main/java/net/okocraft/boxtradestick/BoxUtil.java deleted file mode 100644 index 296bc70..0000000 --- a/src/main/java/net/okocraft/boxtradestick/BoxUtil.java +++ /dev/null @@ -1,116 +0,0 @@ -package net.okocraft.boxtradestick; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import net.okocraft.box.api.BoxAPI; -import net.okocraft.box.api.event.stockholder.stock.StockEvent; -import net.okocraft.box.api.model.item.BoxItem; -import net.okocraft.box.api.model.stock.StockHolder; -import net.okocraft.box.feature.stick.StickFeature; -import net.okocraft.box.feature.stick.item.BoxStickItem; -import org.bukkit.GameMode; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.MerchantRecipe; -import org.jetbrains.annotations.NotNull; - -public final class BoxUtil { - - private BoxUtil() {} - - public static Optional getStock(Player player) { - var playerMap = BoxAPI.api().getBoxPlayerMap(); - - if (!playerMap.isLoaded(player)) { - return Optional.empty(); - } - - return Optional.of(playerMap.get(player).getCurrentStockHolder()); - } - - public static boolean tryConsumingStockMulti(Player player, List items, Trade cause) { - var stock = getStock(player); - if (stock.isEmpty()) { - return false; - } - - items = new ArrayList<>(items); - items.removeIf(i -> i.getType().isAir()); - - var requiredItems = new ConcurrentHashMap(); - for (ItemStack ingredient: items) { - var boxItem = Optional.ofNullable(ingredient).flatMap(BoxUtil::getBoxItem); - if (boxItem.isEmpty()) { - return false; - } - if (stock.get().getAmount(boxItem.get()) < ingredient.getAmount()) { - return false; - } - requiredItems.put(boxItem.get(), ingredient.getAmount()); - } - - for (var entry : requiredItems.entrySet()) { - stock.get().decrease(entry.getKey(), entry.getValue(), cause); - } - - return true; - } - - public static int calcConsumedAmount(Player player, List items) { - var stock = getStock(player); - if (stock.isEmpty()) { - return 0; - } - - items = new ArrayList<>(items); - items.removeIf(i -> i.getType().isAir()); - - int consumingAmount = Integer.MAX_VALUE; - for (ItemStack ingredient: items) { - var boxItem = Optional.ofNullable(ingredient).flatMap(BoxUtil::getBoxItem); - if (boxItem.isEmpty()) { - return 0; - } - consumingAmount = Math.min(consumingAmount, stock.get().getAmount(boxItem.get()) / ingredient.getAmount()); - } - - return consumingAmount; - } - - public static Optional getBoxItem(ItemStack item) { - return BoxAPI.api().getItemManager().getBoxItem(item); - } - - public static BoxStickItem getBoxStickItem() { - return BoxAPI.api().getFeatureProvider().getFeature(StickFeature.class) - .orElseThrow(() -> new IllegalStateException("Failed to load boxStickItem.")) - .getBoxStickItem(); - } - - public static boolean checkPlayerCondition(@NotNull Player player, @NotNull String permissionNode) { - if (player.getGameMode() != GameMode.ADVENTURE && - player.getGameMode() != GameMode.SURVIVAL) { - return false; - } - - if (!BoxAPI.api().canUseBox(player)) { - return false; - } - - if (!player.hasPermission(permissionNode)) { - return false; - } - - return getBoxStickItem().check(player.getInventory().getItemInOffHand()) - || getBoxStickItem().check(player.getInventory().getItemInMainHand()); - } - - public record Trade(@NotNull Player trader, @NotNull MerchantRecipe merchantRecipe) implements StockEvent.Cause { - @Override - public @NotNull String name() { - return "trade"; - } - } -} diff --git a/src/main/java/net/okocraft/boxtradestick/ItemUtil.java b/src/main/java/net/okocraft/boxtradestick/ItemUtil.java deleted file mode 100644 index a849af3..0000000 --- a/src/main/java/net/okocraft/boxtradestick/ItemUtil.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.okocraft.boxtradestick; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.stream.StreamSupport; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.translation.GlobalTranslator; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class ItemUtil { - - private ItemUtil() {} - - public static void addLoreOfStock(Player trader, ItemStack itemToCheck, ItemStack itemToApply, boolean useInventoryIfInvalidItem) { - List lore = itemToApply.lore(); - if (lore == null) { - lore = new ArrayList<>(); - } else { - lore = new ArrayList<>(lore); - } - - if (BoxUtil.getBoxItem(itemToCheck).isPresent()) { - lore.add(Translatables.GUI_CURRENT_STOCK.apply(trader, itemToCheck)); - } else if (useInventoryIfInvalidItem) { - int stock = StreamSupport.stream(trader.getInventory().spliterator(), false) - .filter(itemToCheck::isSimilar) - .map(ItemStack::getAmount) - .reduce(Integer::sum).orElse(0); - lore.add(Translatables.GUI_CURRENT_STOCK_RAW.apply(stock)); - } - lore(trader.locale(), itemToApply, lore); - } - - public static void lore(Locale locale, @NotNull ItemStack item, @Nullable List lore) { - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - if (lore != null) { - lore = new ArrayList<>(lore); - lore.replaceAll(line -> GlobalTranslator.render(line, locale)); - } - meta.lore(lore); - item.setItemMeta(meta); - } - } - - public static void displayName(Locale locale, @NotNull ItemStack item, @Nullable Component displayName) { - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - if (displayName != null) { - displayName = GlobalTranslator.render(displayName, locale); - } - meta.displayName(displayName); - item.setItemMeta(meta); - } - } - - @Nullable - public static ItemStack create(@NotNull Material material) { - if (!material.isItem()) { - return null; - } - return new ItemStack(material); - } - - @Nullable - public static ItemStack create(Locale locale, @NotNull Material material, @Nullable Component displayName) { - ItemStack created = create(material); - if (created == null) { - return null; - } - displayName(locale, created, displayName); - return created; - } - - @Nullable - public static ItemStack create(Locale locale, @NotNull Material material, @Nullable Component displayName, @Nullable List lore) { - ItemStack created = create(locale, material, displayName); - if (created == null) { - return null; - } - lore(locale, created, lore); - return created; - } -} diff --git a/src/main/java/net/okocraft/boxtradestick/Languages.java b/src/main/java/net/okocraft/boxtradestick/Languages.java new file mode 100644 index 0000000..6e5dd13 --- /dev/null +++ b/src/main/java/net/okocraft/boxtradestick/Languages.java @@ -0,0 +1,98 @@ +package net.okocraft.boxtradestick; + +import com.github.siroshun09.messages.minimessage.arg.Arg1; +import com.github.siroshun09.messages.minimessage.arg.Arg2; +import com.github.siroshun09.messages.minimessage.arg.Arg3; +import com.github.siroshun09.messages.minimessage.base.MiniMessageBase; +import com.github.siroshun09.messages.minimessage.base.Placeholder; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.AbstractVillager; +import org.bukkit.entity.Player; +import org.bukkit.entity.Villager; +import org.bukkit.entity.WanderingTrader; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +import static com.github.siroshun09.messages.minimessage.arg.Arg1.arg1; +import static com.github.siroshun09.messages.minimessage.arg.Arg2.arg2; +import static com.github.siroshun09.messages.minimessage.arg.Arg3.arg3; +import static com.github.siroshun09.messages.minimessage.base.MiniMessageBase.messageKey; + +public final class Languages { + + private static final Map DEFAULT_MESSAGES = new LinkedHashMap<>(); + private static final Placeholder PROFESSION = Placeholder.component( + "profession", + abstractVillager -> switch (abstractVillager) { + case Villager villager -> Component.translatable(villager.getProfession()); + case WanderingTrader trader -> Component.translatable(trader.getType()); + default -> Component.text("Custom"); + } + ); + private static final Placeholder VILLAGER_NAME = Placeholder.component( + "name", + abstractVillager -> { + var customName = abstractVillager.customName(); + return customName != null ? customName : Component.text(abstractVillager.getUniqueId().toString().substring(0, 9) + "..."); + } + ); + private static final Placeholder ITEM_NAME = Placeholder.component("item", item -> { + var customName = item.hasItemMeta() ? item.getItemMeta().displayName() : null; + return customName != null ? customName : Component.translatable(item); + }); + private static final Placeholder AMOUNT = Placeholder.component("amount", Component::text); + private static final Placeholder COOLDOWN_MILLIS_TO_SECONDS = Placeholder.component("cooldown", cooldown -> Component.text(((double) cooldown) / 1000)); + private static final Placeholder PRICE = Placeholder.component("price", Component::text); + private static final Placeholder NEW_PRICE = Placeholder.component("new_price", Component::text); + private static final Placeholder TIMES = Placeholder.component("times", Component::text); + private static final Placeholder KIND = Placeholder.component("kind", Component::text); + + public static final Arg2 GUI_TITLE = arg2(def("gui.title", " ()"), PROFESSION, VILLAGER_NAME); + public static final Arg1 GUI_CURRENT_STOCK = arg1(def("gui.current-stock", "Stock: "), AMOUNT); + public static final Arg2 GUI_PRICE_DIFF = arg2(def("gui.price-diff", ""), PRICE, NEW_PRICE); + public static final MiniMessageBase RECIPE_SELECTED = messageKey(def("gui.recipe.selected", "Selected")); + public static final MiniMessageBase RECIPE_NOT_SELECTED = messageKey(def("gui.recipe.not-selected", "Not selected")); + public static final MiniMessageBase RECIPE_LORE = messageKey(def("gui.recipe.lore", "You can trade item for max uses by hitting villagerswith holding box stick after select offer.")); + public static final MiniMessageBase RECIPE_OUT_OF_STOCK = messageKey(def("gui.recipe.out-of-stock", "Out of stock!")); + public static final Arg1 GUI_RESULT_NAME_AND_OUT_OF_STOCK = arg1(def("gui.result.name-and-out-of-stock", " (Out of stock!)"), ITEM_NAME); + public static final Arg1 GUI_RESULT_BULK_TRADE = arg1(def("gui.result.bulk-trade", "Bulk trade "), ITEM_NAME); + public static final MiniMessageBase SCROLL_UP = messageKey(def("gui.scroll.up-arrow", "↑")); + public static final MiniMessageBase SCROLL_DOWN = messageKey(def("gui.scroll.down-arrow", "↓")); + + public static final Arg3 RESULT_TIMES = arg3(def("message.result-times.single", "Traded times and got x."), TIMES, ITEM_NAME, AMOUNT); + public static final Arg2 MULTIPLE_RESULT_TIMES = arg2(def("message.result-times.multiple", "Total of traded the kind of items."), TIMES, KIND); + public static final Arg1 HIT_TRADING_COOLDOWN = arg1(def("message.hit-trading-cooldown", "Hit trading is now in cooldown for second(s)."), COOLDOWN_MILLIS_TO_SECONDS); + + @Contract("_, _ -> param1") + private static @NotNull String def(@NotNull String key, @NotNull String msg) { + DEFAULT_MESSAGES.put(key, msg); + return key; + } + + @Contract(pure = true) + public static @NotNull @UnmodifiableView Map defaultMessages() { + return Collections.unmodifiableMap(DEFAULT_MESSAGES); + } + + public static @NotNull Locale getLocaleFrom(@Nullable Object obj) { + if (obj instanceof Locale locale) { + return locale; + } else if (obj instanceof Player player) { + return player.locale(); + } else { + return Locale.getDefault(); + } + } + + private Languages() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/net/okocraft/boxtradestick/MerchantRecipesGUI.java b/src/main/java/net/okocraft/boxtradestick/MerchantRecipesGUI.java index ef1d5e5..56e7909 100644 --- a/src/main/java/net/okocraft/boxtradestick/MerchantRecipesGUI.java +++ b/src/main/java/net/okocraft/boxtradestick/MerchantRecipesGUI.java @@ -1,12 +1,15 @@ package net.okocraft.boxtradestick; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.UUID; +import java.util.stream.StreamSupport; +import com.github.siroshun09.messages.minimessage.base.MiniMessageBase; +import com.github.siroshun09.messages.minimessage.source.MiniMessageSource; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.okocraft.box.api.BoxAPI; +import net.okocraft.box.feature.gui.api.util.ItemEditor; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Sound; @@ -16,6 +19,7 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.MerchantRecipe; +import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -23,21 +27,20 @@ public class MerchantRecipesGUI implements InventoryHolder { + private static final ItemStack NON_BUTTON = ItemEditor.create().displayName(Component.empty().color(NamedTextColor.BLACK)).createItem(Material.GRAY_STAINED_GLASS_PANE); private static final Class CUSTOM_INVENTORY_CLASS; static { CUSTOM_INVENTORY_CLASS = Bukkit.createInventory(null, 54, Component.empty()).getClass(); } - public static MerchantRecipesGUI fromTopInventory(Inventory topInventory) { - if (CUSTOM_INVENTORY_CLASS.isInstance(topInventory) && topInventory.getHolder() instanceof MerchantRecipesGUI gui) { - return gui; - } else { - return null; - } + public static MerchantRecipesGUI fromInventory(Inventory inventory) { + return CUSTOM_INVENTORY_CLASS.isInstance(inventory) && inventory.getHolder() instanceof MerchantRecipesGUI gui ? gui : null; } private final Player trader; + private final MiniMessageSource messageSource; + private final CachedItems cachedItems; private final UUID worldUid; private final UUID villagerUuid; @@ -45,11 +48,13 @@ public static MerchantRecipesGUI fromTopInventory(Inventory topInventory) { private boolean closed; - public MerchantRecipesGUI(@NotNull Player trader, @NotNull AbstractVillager villager) { + public MerchantRecipesGUI(@NotNull Player trader, @NotNull MiniMessageSource messageSource, @NotNull AbstractVillager villager) { this.trader = trader; + this.messageSource = messageSource; + this.cachedItems = new CachedItems(messageSource); this.worldUid = villager.getWorld().getUID(); this.villagerUuid = villager.getUniqueId(); - this.inventory = Bukkit.createInventory(this, 54, Translatables.GUI_TITLE.apply(villager)); + this.inventory = Bukkit.createInventory(this, 54, Languages.GUI_TITLE.apply(villager, villager).create(messageSource)); initialize(villager); } @@ -60,14 +65,14 @@ public MerchantRecipesGUI(@NotNull Player trader, @NotNull AbstractVillager vill } private void initialize(@NotNull AbstractVillager villager) { - ItemStack[] filled = inventory.getContents(); - Arrays.fill(filled, createNonButton()); - getInventory().setContents(filled); + ItemStack[] filled = this.inventory.getContents(); + Arrays.fill(filled, NON_BUTTON); + this.inventory.setContents(filled); - inventory.setItem(17, createArrow(Translatables.GUI_SCROLL_UP_ARROW)); - inventory.setItem(44, createArrow(Translatables.GUI_SCROLL_DOWN_ARROW)); + this.inventory.setItem(17, this.createArrow(Languages.SCROLL_UP)); + this.inventory.setItem(44, this.createArrow(Languages.SCROLL_DOWN)); - update(villager, TradeStickData.loadFrom(villager)); + this.update(villager, TradeStickData.loadFrom(villager)); } public void scheduleWatchingTask() { @@ -90,16 +95,8 @@ public void scheduleWatchingTask() { ); } - private ItemStack createNonButton() { - return ItemUtil.create( - trader.locale(), - Material.GRAY_STAINED_GLASS_PANE, - Component.empty().color(NamedTextColor.BLACK) - ); - } - - private ItemStack createArrow(Component name) { - return ItemUtil.create(trader.locale(), Material.ARROW, name); + private ItemStack createArrow(MiniMessageBase displayName) { + return ItemEditor.create().displayName(displayName.create(this.messageSource)).createItem(Material.ARROW); } private void update(@NotNull AbstractVillager villager, @NotNull TradeStickData data) { @@ -115,62 +112,53 @@ private void update(@NotNull AbstractVillager villager, @NotNull TradeStickData } private void updateTradeItem(@NotNull AbstractVillager villager, int row, int recipeIndex, boolean selected) { - if (selected) { - inventory.setItem(row * 9, ItemUtil.create( - trader.locale(), - Material.LIME_WOOL, - Translatables.GUI_RECIPE_SELECTED, - Translatables.GUI_RECIPE_SELECTED_LORE - )); - } else { - inventory.setItem(row * 9, ItemUtil.create( - trader.locale(), - Material.RED_STAINED_GLASS, - Translatables.GUI_RECIPE_NOT_SELECTED, - Translatables.GUI_RECIPE_SELECTED_LORE - )); - } + this.inventory.setItem(row * 9, selected ? this.cachedItems.recipeSelected : this.cachedItems.recipeNotSelected); MerchantRecipe recipe = villager.getRecipe(recipeIndex); - List ingredients = new ArrayList<>(recipe.getIngredients()); - for (int i = 0; i < ingredients.size(); i++) { - ItemStack originalIngredient = ingredients.get(i); - ItemStack ingredient = originalIngredient.clone(); + + List ingredients = recipe.getIngredients(); + for (int i = 0, size = ingredients.size(); i < size; i++) { + var editor = ItemEditor.create(); + var ingredient = ingredients.get(i).clone(); + editor.copyLoreFrom(ingredient); if (i == 0) { int originalPrice = ingredient.getAmount(); - recipe.adjust(ingredient); - ingredients.set(i, ingredient.clone()); - int currentPrice = ingredient.getAmount(); - if (originalPrice != currentPrice) { - ItemUtil.lore(trader.locale(), ingredient, - List.of(Translatables.GUI_PRICE_DIFF.apply(originalPrice, currentPrice))); + var adjusted = recipe.getAdjustedIngredient1(); + + if (adjusted != null && originalPrice != adjusted.getAmount()) { + editor.loreLine(Languages.GUI_PRICE_DIFF.apply(ingredient.getAmount(), adjusted.getAmount()).create(this.messageSource)); + ingredient.setAmount(adjusted.getAmount()); } } - ItemUtil.addLoreOfStock(trader, originalIngredient, ingredient, false); - inventory.setItem(row * 9 + 2 + i, ingredient); + int currentStock = this.getCurrentStock(ingredient, false); + if (currentStock != -1) { + editor.loreLine(Languages.GUI_CURRENT_STOCK.apply(currentStock).create(this.messageSource)); + } + + this.inventory.setItem(row * 9 + 2 + i, editor.applyTo(ingredient)); + } + + if (ingredients.size() == 1) { + this.inventory.setItem(row * 9 + 3, NON_BUTTON); } - ItemStack result; + ItemStack resultIcon; + ItemEditor editor; + int leftUses = recipe.getMaxUses() - recipe.getUses(); if (0 < leftUses) { - result = recipe.getResult().clone(); - ItemUtil.addLoreOfStock(trader, recipe.getResult(), result, true); - inventory.setItem(row * 9 + 5, result.clone()); - ItemUtil.displayName(trader.locale(), result, Translatables.GUI_RESULT_BULK_TRADE.apply(result)); - result.setAmount(Math.max(1, Math.min(leftUses, BoxUtil.calcConsumedAmount(trader, ingredients)))); + resultIcon = recipe.getResult().asQuantity(Math.max(1, Math.min(leftUses, this.calcConsumedAmount(ingredients)))); + editor = ItemEditor.create().displayName(Languages.GUI_RESULT_BULK_TRADE.apply(resultIcon).create(this.messageSource)).copyLoreFrom(resultIcon); } else { - result = Objects.requireNonNull(ItemUtil.create( - trader.locale(), - Material.BARRIER, - Translatables.GUI_RESULT_NAME_OUT_OF_STOCK.apply(recipe.getResult())) - ); - ItemUtil.addLoreOfStock(trader, recipe.getResult(), result, true); - inventory.setItem(row * 9 + 5, result.clone()); + resultIcon = new ItemStack(Material.BARRIER); + editor = ItemEditor.create().displayName(Languages.GUI_RESULT_NAME_AND_OUT_OF_STOCK.apply(recipe.getResult()).create(this.messageSource)); } - inventory.setItem(row * 9 + 6, result); + editor.loreLine(Languages.GUI_CURRENT_STOCK.apply(this.getCurrentStock(recipe.getResult(), true)).create(this.messageSource)).applyTo(resultIcon); + this.inventory.setItem(row * 9 + 5, resultIcon.getAmount() == 1 ? resultIcon : resultIcon.asOne()); + this.inventory.setItem(row * 9 + 6, resultIcon); } public void onClick(int slot) { @@ -273,7 +261,7 @@ private int getScroll(int maxScroll, @NotNull TradeStickData data) { } private boolean isSilentlyClosed() { - return !closed && this != MerchantRecipesGUI.fromTopInventory(trader.getOpenInventory().getTopInventory()); + return !closed && this != MerchantRecipesGUI.fromInventory(trader.getOpenInventory().getTopInventory()); } private boolean shouldClose() { @@ -283,10 +271,10 @@ private boolean shouldClose() { @Contract("null -> true") private boolean shouldClose(@Nullable AbstractVillager villager) { return villager == null || - villager.isDead() || - !villager.getWorld().equals(trader.getWorld()) || - 100 < villager.getLocation().distanceSquared(trader.getLocation()) || - !trader.equals(villager.getTrader()); + villager.isDead() || + !villager.getWorld().equals(trader.getWorld()) || + 100 < villager.getLocation().distanceSquared(trader.getLocation()) || + !trader.equals(villager.getTrader()); } private void tryStopTrading() { @@ -302,4 +290,56 @@ private void tryStopTrading(@NotNull AbstractVillager villager) { NMSUtil.stopTrading(villager); } } + + private int calcConsumedAmount(List ingredients) { + if (!BoxAPI.api().getBoxPlayerMap().isLoaded(this.trader)) { + return 0; + } + + var stockHolder = BoxAPI.api().getBoxPlayerMap().get(this.trader).getCurrentStockHolder(); + int consumingAmount = Integer.MAX_VALUE; + + for (var ingredient : ingredients) { + if (ingredient.getType().isAir()) { + continue; + } + + var boxItem = BoxAPI.api().getItemManager().getBoxItem(ingredient); + + if (boxItem.isEmpty()) { + return 0; + } else { + consumingAmount = Math.min(consumingAmount, stockHolder.getAmount(boxItem.get()) / ingredient.getAmount()); + } + } + + return consumingAmount; + } + + private int getCurrentStock(@NotNull ItemStack item, boolean useInventoryIfNotBoxItem) { + var boxItem = BoxAPI.api().getItemManager().getBoxItem(item); + var playerMap = BoxAPI.api().getBoxPlayerMap(); + + if (boxItem.isPresent() && playerMap.isLoaded(this.trader)) { + return playerMap.get(this.trader).getCurrentStockHolder().getAmount(boxItem.get()); + } else if (useInventoryIfNotBoxItem) { + return StreamSupport.stream(this.trader.getInventory().spliterator(), false) + .filter(item::isSimilar) + .mapToInt(ItemStack::getAmount) + .reduce(Integer::sum).orElse(0); + } else { + return -1; + } + } + + private static class CachedItems { + private final ItemStack recipeSelected; + private final ItemStack recipeNotSelected; + + private CachedItems(@NotNull MiniMessageSource messageSource) { + var selectedLore = Languages.RECIPE_LORE.create(messageSource); + this.recipeSelected = ItemEditor.create().displayName(Languages.RECIPE_SELECTED.create(messageSource)).loreLines(selectedLore).createItem(Material.LIME_WOOL); + this.recipeNotSelected = ItemEditor.create().displayName(Languages.RECIPE_NOT_SELECTED.create(messageSource)).loreLines(selectedLore).createItem(Material.RED_STAINED_GLASS); + } + } } diff --git a/src/main/java/net/okocraft/boxtradestick/PlayerListener.java b/src/main/java/net/okocraft/boxtradestick/PlayerListener.java index 1cd1281..707d8de 100644 --- a/src/main/java/net/okocraft/boxtradestick/PlayerListener.java +++ b/src/main/java/net/okocraft/boxtradestick/PlayerListener.java @@ -1,9 +1,14 @@ package net.okocraft.boxtradestick; +import java.util.EnumSet; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import com.github.siroshun09.messages.minimessage.localization.MiniMessageLocalization; +import net.okocraft.box.api.BoxAPI; +import net.okocraft.box.feature.stick.item.BoxStickItem; import org.bukkit.Bukkit; +import org.bukkit.GameMode; import org.bukkit.entity.AbstractVillager; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; @@ -18,14 +23,24 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.MerchantInventory; +import org.jetbrains.annotations.NotNull; public class PlayerListener implements Listener { private static final long TRADE_COOLDOWN_TIME = 1000L; private static final long TRADE_COOLDOWN_TIME_AFTER_THE_2ND = 500L; + private static final EnumSet IGNORING_GAME_MODES = EnumSet.of(GameMode.CREATIVE, GameMode.SPECTATOR); + + private final BoxStickItem boxStickItem; + private final MiniMessageLocalization localization; private final Map tradeCooldownEndTimeMap = new ConcurrentHashMap<>(); - private volatile boolean onEntityDamageByEntityEvent = false; + private final ThreadLocal calledPlayerInteractEntityEvent = new ThreadLocal<>(); + + public PlayerListener(BoxStickItem boxStickItem, MiniMessageLocalization localization) { + this.boxStickItem = boxStickItem; + this.localization = localization; + } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { @@ -34,35 +49,34 @@ public void onPlayerQuit(PlayerQuitEvent event) { @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) public void onPlayerAttackVillager(EntityDamageByEntityEvent event) { - if (!(event.getDamager() instanceof Player player)) { - return; - } - - if (!(event.getEntity() instanceof AbstractVillager villager)) { + if (!(event.getDamager() instanceof Player player) || !(event.getEntity() instanceof AbstractVillager villager)) { return; } - if (!BoxUtil.checkPlayerCondition(player, "boxtradestick.trade")) { + if (!this.hasBoxStick(player)) { return; } event.setCancelled(true); - if (!TradeProcessor.canTradeByStick(villager)) { + if (shouldBlockEventProcess(player) || !TradeProcessor.canTradeByStick(villager)) { return; } + var messageSource = this.localization.findSource(player); + long cooldownTime = calcCooldownTime(player); if (cooldownTime > 0) { - player.sendActionBar(Translatables.HIT_TRADING_COOLDOWN.apply(cooldownTime)); + player.sendActionBar(Languages.HIT_TRADING_COOLDOWN.apply(cooldownTime).create(messageSource)); return; } - onEntityDamageByEntityEvent = true; - if (!new PlayerInteractEntityEvent(player, villager).callEvent()) { + var playerInteractEvent = new PlayerInteractEntityEvent(player, villager); + this.calledPlayerInteractEntityEvent.set(playerInteractEvent); + if (!playerInteractEvent.callEvent()) { return; } - onEntityDamageByEntityEvent = false; + this.calledPlayerInteractEntityEvent.remove(); checkCurrentTrader(villager); @@ -72,7 +86,7 @@ public void onPlayerAttackVillager(EntityDamageByEntityEvent event) { NMSUtil.startTrading(player, villager); - int succeededCount = TradeProcessor.processSelectedOffersForMaxUses(player, villager); + int succeededCount = TradeProcessor.processSelectedOffersForMaxUses(player, messageSource, villager); if (succeededCount > 0) { long cooldownEndTime = calcCooldownEndTime(succeededCount); tradeCooldownEndTimeMap.put(player.getUniqueId(), cooldownEndTime); @@ -83,25 +97,22 @@ public void onPlayerAttackVillager(EntityDamageByEntityEvent event) { @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { - if (onEntityDamageByEntityEvent || !(event.getRightClicked() instanceof AbstractVillager villager) || !TradeProcessor.canTradeByStick(villager)) { + if (this.calledPlayerInteractEntityEvent.get() == event || !(event.getRightClicked() instanceof AbstractVillager villager)) { return; } Player player = event.getPlayer(); - if (!BoxUtil.checkPlayerCondition(player, "boxtradestick.trade")) { - return; - } - - event.setCancelled(true); - - checkCurrentTrader(villager); + if (TradeProcessor.canTradeByStick(villager) && this.hasBoxStick(player) && !shouldBlockEventProcess(player)) { + event.setCancelled(true); + checkCurrentTrader(villager); - if (NMSUtil.simulateMobInteract(player, villager, event.getHand())) { - NMSUtil.startTrading(player, villager); + if (NMSUtil.simulateMobInteract(player, villager, event.getHand())) { + NMSUtil.startTrading(player, villager); - MerchantRecipesGUI gui = new MerchantRecipesGUI(player, villager); - player.openInventory(gui.getInventory()); - gui.scheduleWatchingTask(); + MerchantRecipesGUI gui = new MerchantRecipesGUI(player, this.localization.findSource(player), villager); + player.openInventory(gui.getInventory()); + gui.scheduleWatchingTask(); + } } } @@ -110,7 +121,7 @@ public void onInventoryClick(InventoryClickEvent event) { if (event.getClickedInventory() == null) { return; } - MerchantRecipesGUI gui = MerchantRecipesGUI.fromTopInventory(event.getView().getTopInventory()); + MerchantRecipesGUI gui = MerchantRecipesGUI.fromInventory(event.getView().getTopInventory()); if (gui != null) { event.setCancelled(true); gui.onClick(event.getSlot()); @@ -119,7 +130,7 @@ public void onInventoryClick(InventoryClickEvent event) { @EventHandler(ignoreCancelled = true) public void onInventoryClose(InventoryCloseEvent event) { - MerchantRecipesGUI gui = MerchantRecipesGUI.fromTopInventory(event.getView().getTopInventory()); + MerchantRecipesGUI gui = MerchantRecipesGUI.fromInventory(event.getView().getTopInventory()); if (gui != null) { gui.onClose(); @@ -135,7 +146,12 @@ private long calcCooldownTime(Player player) { return cooldownTime - System.currentTimeMillis(); } - private void checkCurrentTrader(AbstractVillager villager) { + private boolean hasBoxStick(@NotNull Player player) { + return this.boxStickItem.check(player.getInventory().getItemInMainHand()) || + this.boxStickItem.check(player.getInventory().getItemInOffHand()); + } + + private static void checkCurrentTrader(AbstractVillager villager) { HumanEntity trader = villager.getTrader(); if (trader == null) { @@ -143,7 +159,7 @@ private void checkCurrentTrader(AbstractVillager villager) { } Inventory inv = trader.getOpenInventory().getTopInventory(); - MerchantRecipesGUI gui = MerchantRecipesGUI.fromTopInventory(inv); + MerchantRecipesGUI gui = MerchantRecipesGUI.fromInventory(inv); if (gui != null && villager.equals(gui.getVillager())) { return; @@ -159,4 +175,8 @@ private void checkCurrentTrader(AbstractVillager villager) { NMSUtil.stopTrading(villager); // cleanup current trader } + + private static boolean shouldBlockEventProcess(@NotNull Player player) { + return IGNORING_GAME_MODES.contains(player.getGameMode()) || !BoxAPI.api().canUseBox(player) || !player.hasPermission("boxtradestick.trade"); + } } diff --git a/src/main/java/net/okocraft/boxtradestick/TradeCause.java b/src/main/java/net/okocraft/boxtradestick/TradeCause.java new file mode 100644 index 0000000..7861b40 --- /dev/null +++ b/src/main/java/net/okocraft/boxtradestick/TradeCause.java @@ -0,0 +1,15 @@ +package net.okocraft.boxtradestick; + +import net.okocraft.box.api.event.stockholder.stock.StockEvent; +import org.bukkit.entity.Player; +import org.bukkit.inventory.MerchantRecipe; +import org.jetbrains.annotations.NotNull; + +public record TradeCause(@NotNull Player trader, @NotNull MerchantRecipe merchantRecipe) implements StockEvent.Cause { + + @Override + public @NotNull String name() { + return "trade"; + } + +} diff --git a/src/main/java/net/okocraft/boxtradestick/TradeProcessor.java b/src/main/java/net/okocraft/boxtradestick/TradeProcessor.java index 0c5b0c6..79e890b 100644 --- a/src/main/java/net/okocraft/boxtradestick/TradeProcessor.java +++ b/src/main/java/net/okocraft/boxtradestick/TradeProcessor.java @@ -1,9 +1,14 @@ package net.okocraft.boxtradestick; +import com.github.siroshun09.messages.minimessage.source.MiniMessageSource; import io.papermc.paper.event.player.PlayerPurchaseEvent; import io.papermc.paper.event.player.PlayerTradeEvent; import java.util.ArrayList; import java.util.List; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +import net.kyori.adventure.text.Component; +import net.okocraft.box.api.BoxAPI; +import net.okocraft.box.api.model.item.BoxItem; import net.okocraft.box.api.util.InventoryUtil; import org.bukkit.Sound; import org.bukkit.entity.AbstractVillager; @@ -19,7 +24,7 @@ public static boolean canTradeByStick(@NotNull AbstractVillager villager) { return villager.getRecipeCount() < TradeStickData.MAXIMUM_INDEX; } - public static int processSelectedOffersForMaxUses(@NotNull Player trader, @NotNull AbstractVillager villager) { + public static int processSelectedOffersForMaxUses(@NotNull Player trader, @NotNull MiniMessageSource messageSource, @NotNull AbstractVillager villager) { int[] recipeIndices = TradeStickData.loadFrom(villager).getSelectedIndices(trader.getUniqueId()); if (recipeIndices.length == 0) { @@ -38,31 +43,27 @@ public static int processSelectedOffersForMaxUses(@NotNull Player trader, @NotNu if (results.isEmpty()) { trader.playSound(villager, Sound.ENTITY_VILLAGER_NO, 1, 1); - trader.sendActionBar(Translatables.OUT_OF_STOCK); + trader.sendActionBar(Languages.RECIPE_OUT_OF_STOCK.create(messageSource)); return 0; } - trader.playSound(villager, Sound.ENTITY_VILLAGER_TRADE, 1, 1); + Component message; if (results.size() == 1) { - TradeResult result = results.get(0); - trader.sendActionBar(Translatables.RESULT_TIMES.apply(result.count, result.recipe.getResult())); + TradeResult result = results.getFirst(); + message = Languages.RESULT_TIMES.apply(result.count, result.recipe.getResult(), result.recipe.getResult().getAmount() * result.count).create(messageSource); } else { - trader.sendActionBar(Translatables.MULTIPLE_RESULT_TIMES.apply( - results.stream().mapToInt(TradeResult::count).sum(), - results.size() - )); + message = Languages.MULTIPLE_RESULT_TIMES.apply(results.stream().mapToInt(TradeResult::count).sum(), results.size()).create(messageSource); } + trader.playSound(villager, Sound.ENTITY_VILLAGER_TRADE, 1, 1); + trader.sendActionBar(message); + return results.size(); } public static @Nullable TradeResult tradeForMaxUses(@NotNull Player trader, @NotNull AbstractVillager villager, int recipeIndex) { - if (!BoxUtil.checkPlayerCondition(trader, "boxtradestick.trade")) { - return null; - } - - if (recipeIndex < 0 || recipeIndex >= villager.getRecipeCount()) { + if (!canTrade(trader, villager, recipeIndex)) { return null; } @@ -79,18 +80,18 @@ public static int processSelectedOffersForMaxUses(@NotNull Player trader, @NotNu } public static boolean trade(@NotNull Player trader, @NotNull AbstractVillager villager, int recipeIndex) { - if (!BoxUtil.checkPlayerCondition(trader, "boxtradestick.trade")) { + if (canTrade(trader, villager, recipeIndex)) { + return trade0(trader, villager, recipeIndex); + } else { return false; } + } - if (recipeIndex < 0 || recipeIndex >= villager.getRecipeCount()) { + private static boolean trade0(@NotNull Player trader, @NotNull AbstractVillager villager, int recipeIndex) { + if (!BoxAPI.api().getBoxPlayerMap().isLoaded(trader)) { return false; } - return trade0(trader, villager, recipeIndex); - } - - private static boolean trade0(@NotNull Player trader, @NotNull AbstractVillager villager, int recipeIndex) { var merchantOffer = villager.getRecipe(recipeIndex); if (merchantOffer.getUses() >= merchantOffer.getMaxUses()) { @@ -104,31 +105,59 @@ private static boolean trade0(@NotNull Player trader, @NotNull AbstractVillager } merchantOffer = event.getTrade(); - List ingredients = new ArrayList<>(merchantOffer.getIngredients()); - if (ingredients.size() >= 1) { - ingredients.set(0, merchantOffer.getAdjustedIngredient1()); - } - var cause = new BoxUtil.Trade(trader, merchantOffer); - if (!BoxUtil.tryConsumingStockMulti(trader, ingredients, cause)) { + + var ingredients = merchantOffer.getIngredients(); + + if (ingredients.isEmpty()) { return false; } - var resultBukkit = merchantOffer.getResult(); + var ingredientMap = new Object2IntArrayMap(ingredients.size()); - var result = BoxUtil.getBoxItem(resultBukkit); - if (result.isEmpty()) { - int remaining = InventoryUtil.putItems(trader.getInventory(), resultBukkit, resultBukkit.getAmount()); + for (int i = 0, size = ingredients.size(); i < size; i++) { + var ingredient = ingredients.get(i); - if (0 < remaining) { - drop(trader, resultBukkit.asQuantity(remaining)); + if (ingredient.getType().isAir()) { + continue; + } + + var boxItem = BoxAPI.api().getItemManager().getBoxItem(ingredient); + + if (boxItem.isEmpty()) { + return false; + } else if (i == 0) { + var adjusted = merchantOffer.getAdjustedIngredient1(); + if (adjusted != null) { + ingredientMap.put(boxItem.get(), adjusted.getAmount()); + } else { + return false; + } + } else { + ingredientMap.put(boxItem.get(), ingredient.getAmount()); } - } else { - // stock is definitely present. - BoxUtil.getStock(trader).ifPresent(stock -> stock.increase(result.get(), resultBukkit.getAmount(), cause)); } - NMSUtil.processTrade(trader, villager, merchantOffer, event); - return true; + var stockHolder = BoxAPI.api().getBoxPlayerMap().get(trader).getCurrentStockHolder(); + var cause = new TradeCause(trader, new MerchantRecipe(merchantOffer)); + + if (stockHolder.decreaseIfPossible(ingredientMap, cause)) { + var resultBukkit = merchantOffer.getResult(); + var result = BoxAPI.api().getItemManager().getBoxItem(resultBukkit); + if (result.isEmpty()) { + int remaining = InventoryUtil.putItems(trader.getInventory(), resultBukkit, resultBukkit.getAmount()); + + if (0 < remaining) { + drop(trader, resultBukkit.asQuantity(remaining)); + } + } else { + stockHolder.increase(result.get(), resultBukkit.getAmount(), cause); + } + + NMSUtil.processTrade(trader, villager, merchantOffer, event); + return true; + } else { + return false; + } } private static void drop(Player player, ItemStack item) { @@ -138,6 +167,13 @@ private static void drop(Player player, ItemStack item) { player.getInventory().setItemInMainHand(hand); } + private static boolean canTrade(Player player, @NotNull AbstractVillager villager, int recipeIndex) { + return BoxAPI.api().canUseBox(player) && + player.hasPermission("boxtradestick.trade") && + 0 <= recipeIndex && + recipeIndex < villager.getRecipeCount(); + } + private TradeProcessor() { } diff --git a/src/main/java/net/okocraft/boxtradestick/Translatables.java b/src/main/java/net/okocraft/boxtradestick/Translatables.java deleted file mode 100644 index 158eff1..0000000 --- a/src/main/java/net/okocraft/boxtradestick/Translatables.java +++ /dev/null @@ -1,113 +0,0 @@ -package net.okocraft.boxtradestick; - -import java.util.List; -import com.github.siroshun09.translationloader.argument.DoubleArgument; -import com.github.siroshun09.translationloader.argument.SingleArgument; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TranslatableComponent; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; -import net.okocraft.box.api.BoxAPI; -import org.bukkit.entity.AbstractVillager; -import org.bukkit.entity.Player; -import org.bukkit.entity.Villager; -import org.bukkit.entity.WanderingTrader; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.Merchant; - -public final class Translatables { - - private Translatables() {} - - public static final SingleArgument GUI_TITLE = - merchant -> Component.translatable("gui.title") - .args(getProfession(merchant), getMerchantNameOrUUID(merchant)); - - public static final SingleArgument GUI_RESULT_NAME_OUT_OF_STOCK = - item -> nonItalic("gui.result-name-out-of-stock") - .args(Component.translatable().key(item)) - .color(NamedTextColor.YELLOW); - - public static final SingleArgument GUI_RESULT_BULK_TRADE = - item -> nonItalic("gui.result-bulk-trade") - .args(Component.translatable().key(item).color(NamedTextColor.AQUA)) - .color(NamedTextColor.GOLD); - - public static final SingleArgument GUI_CURRENT_STOCK_RAW = - stock -> nonItalic("gui.current-stock") - .args(Component.text(stock).color(NamedTextColor.AQUA)) - .color(NamedTextColor.GRAY); - - public static final SingleArgument HIT_TRADING_COOLDOWN = - cooldown -> Component.translatable("hit-trading-cooldown") - .args(Component.text(((double) cooldown) / 1000D).color(NamedTextColor.AQUA)) - .color(NamedTextColor.YELLOW); - - public static final DoubleArgument GUI_CURRENT_STOCK = - (player, item) -> BoxAPI.api().getItemManager() - .getBoxItem(item) - .flatMap(i -> BoxUtil.getStock(player).map(stock -> stock.getAmount(i))) - .map(Translatables.GUI_CURRENT_STOCK_RAW::apply) - .orElse(Translatables.GUI_CURRENT_STOCK_RAW.apply(0)); - - public static final DoubleArgument GUI_PRICE_DIFF = - (originalPrice, price) -> nonItalic("gui.price-diff") - .args(Component.text(originalPrice, NamedTextColor.AQUA), Component.text(price, NamedTextColor.AQUA)) - .color(NamedTextColor.GRAY); - - public static final DoubleArgument RESULT_TIMES = - (traded, result) -> Component.translatable("result-times", NamedTextColor.YELLOW).args( - Component.text(traded), - Component.text(result.getAmount() * traded), - Component.translatable(result) - ); - - public static final DoubleArgument MULTIPLE_RESULT_TIMES = - (traded, kinds) -> Component.translatable("multiple-result-times", NamedTextColor.YELLOW) - .args(Component.text(traded), Component.text(kinds)); - - public static final List GUI_RECIPE_SELECTED_LORE = - List.of( - nonItalic("gui.recipe-selected-lore-1").color(NamedTextColor.GRAY), - nonItalic("gui.recipe-selected-lore-2").color(NamedTextColor.GRAY) - ); - - public static final Component GUI_SCROLL_UP_ARROW = nonItalic("gui.scroll-up-arrow") - .decorate(TextDecoration.BOLD) - .color(NamedTextColor.GOLD); - - public static final Component GUI_SCROLL_DOWN_ARROW = nonItalic("gui.scroll-down-arrow") - .decorate(TextDecoration.BOLD) - .color(NamedTextColor.GOLD); - - public static final Component GUI_RECIPE_SELECTED = nonItalic("gui.recipe-selected"); - - public static final Component GUI_RECIPE_NOT_SELECTED = nonItalic("gui.recipe-not-selected"); - - public static final Component OUT_OF_STOCK = nonItalic("gui.result-out-of-stock") - .color(NamedTextColor.YELLOW); - - private static TranslatableComponent nonItalic(String key) { - return Component.translatable(key).decoration(TextDecoration.ITALIC, TextDecoration.State.FALSE); - } - - private static Component getMerchantNameOrUUID(Merchant merchant) { - if (merchant instanceof AbstractVillager villager) { - return villager.customName() == null - ? Component.text(villager.getUniqueId().toString().substring(0, 9) + "...") - : villager.customName(); - } else { - return Component.text("Custom"); - } - } - - private static Component getProfession(Merchant merchant) { - if (merchant instanceof Villager villager) { - return Component.translatable(villager.getProfession().translationKey()); - } else if (merchant instanceof WanderingTrader wanderingTrader) { - return Component.translatable(wanderingTrader.getType().translationKey()); - } else { - return Component.text("Custom"); - } - } -} diff --git a/src/main/resources/en.yml b/src/main/resources/en.yml deleted file mode 100644 index 108df32..0000000 --- a/src/main/resources/en.yml +++ /dev/null @@ -1,16 +0,0 @@ -gui: - title: "{0} ({1})" # Profession (name or UUID) - price-diff: "{0} → {1}" - current-stock: "Stock: {0}" - scroll-up-arrow: "↑" - scroll-down-arrow: "↓" - recipe-selected: "Selected" - recipe-not-selected: "Not selected" - recipe-selected-lore-1: "You can trade item for max uses by hitting villagers" - recipe-selected-lore-2: "with holding box stick after select offer." - result-out-of-stock: "Out of stock!" - result-name-out-of-stock: "{0} (Out of stock!)" - result-bulk-trade: "Bulk trade {0}" -result-times: "Traded {0} times and got {1} {2}s" -multiple-result-times: "Total of {0} traded the {1} kind of items." -hit-trading-cooldown: "Hit trading is now in cooldown for {0} second." \ No newline at end of file diff --git a/src/main/resources/ja.properties b/src/main/resources/ja.properties new file mode 100644 index 0000000..d704e92 --- /dev/null +++ b/src/main/resources/ja.properties @@ -0,0 +1,14 @@ +gui.title= () +gui.current-stock=在庫\: +gui.price-diff= +gui.recipe.selected=選択中 +gui.recipe.not-selected=未選択 +gui.recipe.lore=選択してbox stickを持って殴ると一気に限界まで取引できます。 +gui.recipe.out-of-stock=売り切れ\! +gui.result.name-and-out-of-stock= (売り切れ\!) +gui.result.bulk-trade=を一括取引 +gui.scroll.up-arrow=↑ +gui.scroll.down-arrow=↓ +message.result-times.single=回交易し、個手に入れました。 +message.result-times.multiple=種類のアイテムを合計回交易しました。 +message.hit-trading-cooldown=殴り交易はクールダウン中です。(あと秒) diff --git a/src/main/resources/ja_JP.yml b/src/main/resources/ja_JP.yml deleted file mode 100644 index ecaf71d..0000000 --- a/src/main/resources/ja_JP.yml +++ /dev/null @@ -1,16 +0,0 @@ -gui: - title: "{0} ({1})" # Profession (name or UUID) - price-diff: "{0} → {1}" - current-stock: "在庫: {0}" - scroll-up-arrow: "↑" - scroll-down-arrow: "↓" - recipe-selected: "選択中" - recipe-not-selected: "選択されていません" - recipe-selected-lore-1: "選択してbox stickを持って殴ると" - recipe-selected-lore-2: "一気に限界まで取引できます。" - result-out-of-stock: "売り切れ!" - result-name-out-of-stock: "{0} (売り切れ!)" - result-bulk-trade: "{0}を一括取引" -result-times: "{0}回交易し、{2}を{1}個手に入れました。" -multiple-result-times: "{1}種類のアイテムを合計{0}回交易しました。" -hit-trading-cooldown: "殴り交易はクールダウン中です。(あと{0}秒)" \ No newline at end of file diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..5538cbe --- /dev/null +++ b/src/main/resources/paper-plugin.yml @@ -0,0 +1,12 @@ +name: BoxTradeStick +version: '${projectVersion}' +main: net.okocraft.boxtradestick.BoxTradeStickPlugin +api-version: "${apiVersion}" +folia-supported: true + +dependencies: + server: + Box: + load: BEFORE + required: true + join-classpath: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml deleted file mode 100644 index 26dc4e0..0000000 --- a/src/main/resources/plugin.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: BoxTradeStick -version: '${projectVersion}' -main: net.okocraft.boxtradestick.BoxTradeStickPlugin -api-version: "1.20" -folia-supported: true -depend: - - Box - -permissions: - boxtradestick.trade: - default: true