Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/main/java/org/Little_100/projecte/bedrock/BedrockFormUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package org.Little_100.projecte.bedrock;

import org.Little_100.projecte.ProjectE;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.geysermc.cumulus.form.Form;
import org.geysermc.floodgate.api.FloodgateApi;

import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;

/**
* 基岩版 Form UI 的通用工具类。
*
* 架构说明:
* Velocity 代理 + Floodgate 后端模式下, 我们的插件跑在后端,
* 后端没有 Geyser API, 但有 Floodgate API。
* 所以用 FloodgateApi 来判断玩家是否是基岩版玩家。
* 发送 Form 用 Cumulus (Cumulus 在 Geyser/Floodgate 任何一方都能工作)。
*
* 职责:
* - 检测玩家是否是 Floodgate 识别的基岩版玩家
* - 安全地发送 Cumulus 表单
* - 维护玩家的 Form Session Token, 防止 Form 重放/双击攻击
*/
public final class BedrockFormUtil {

private BedrockFormUtil() {}

// ==================== Floodgate / Cumulus 检测 ====================

private static Boolean floodgateApiAvailable = null;

public static boolean isFloodgateApiAvailable() {
if (floodgateApiAvailable == null) {
try {
Class.forName("org.geysermc.floodgate.api.FloodgateApi");
floodgateApiAvailable = true;
} catch (ClassNotFoundException e) {
floodgateApiAvailable = false;
}
}
return floodgateApiAvailable;
}

/**
* 判断玩家是否是基岩版玩家 (通过 Floodgate 识别)。
* 任何线程都可调用, 不抛异常。Floodgate 未加载时返回 false。
*/
public static boolean isBedrockPlayer(Player player) {
if (player == null) return false;
return isBedrockPlayer(player.getUniqueId());
}

public static boolean isBedrockPlayer(UUID uuid) {
if (uuid == null) return false;
if (!isFloodgateApiAvailable()) return false;
try {
FloodgateApi api = FloodgateApi.getInstance();
if (api == null) return false;
return api.isFloodgatePlayer(uuid);
} catch (Throwable t) {
return false;
}
}

// 保留旧方法名以防别的代码调用 (兼容)
public static boolean isGeyserApiAvailable() {
return isFloodgateApiAvailable();
}

// ==================== 表单发送 ====================

/**
* 发送 Cumulus 表单给基岩版玩家。主线程调用。
*
* Floodgate 直接支持 Cumulus Form 发送, 通过 FloodgatePlayer.sendForm。
*
* @return true 发送成功; false 玩家非基岩版 / Floodgate 未加载 / 发送异常
*/
public static boolean sendForm(Player player, Form form) {
if (!isBedrockPlayer(player)) return false;
if (form == null) return false;
try {
FloodgateApi api = FloodgateApi.getInstance();
if (api == null) return false;
org.geysermc.floodgate.api.player.FloodgatePlayer fp =
api.getPlayer(player.getUniqueId());
if (fp == null) return false;
fp.sendForm(form);
return true;
} catch (Throwable t) {
ProjectE.getInstance().getLogger().log(
Level.WARNING,
"Failed to send Bedrock form to " + player.getName(),
t
);
return false;
}
}

// ==================== 防重放 Session Token ====================

private static final ConcurrentHashMap<UUID, UUID> currentSession = new ConcurrentHashMap<>();

public static UUID newSession(Player player) {
UUID token = UUID.randomUUID();
currentSession.put(player.getUniqueId(), token);
return token;
}

public static boolean isSessionValid(Player player, UUID token) {
if (player == null || token == null) return false;
UUID current = currentSession.get(player.getUniqueId());
return token.equals(current);
}

public static void clearSession(UUID playerUuid) {
if (playerUuid != null) {
currentSession.remove(playerUuid);
}
}

// ==================== 显示辅助 ====================

public static String formatEmc(long emc) {
return String.format("%,d", emc);
}

public static String getDisplayName(ItemStack item) {
if (item == null) return "";
if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
return item.getItemMeta().getDisplayName();
}
return item.getType().name().toLowerCase().replace('_', ' ');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.Little_100.projecte.bedrock.transmutation;

import org.Little_100.projecte.ProjectE;
import org.Little_100.projecte.bedrock.BedrockFormUtil;
import org.Little_100.projecte.bedrock.transmutation.buy.BuyListForm;
import org.Little_100.projecte.bedrock.transmutation.sell.SellEntryForm;
import org.bukkit.entity.Player;
import org.geysermc.cumulus.form.SimpleForm;

import java.util.UUID;

/**
* 基岩版转换桌主菜单。入口: /projecte opentable 或 /opentable
*/
public final class TransmutationMainForm {

private TransmutationMainForm() {}

public static void open(Player player) {
if (player == null || !player.isOnline()) return;

UUID token = BedrockFormUtil.newSession(player);

long emc = ProjectE.getInstance().getDatabaseManager().getPlayerEmc(player.getUniqueId());

SimpleForm form = SimpleForm.builder()
.title("§5§l转换桌")
.content("§f你的 EMC: §e" + BedrockFormUtil.formatEmc(emc) + "\n\n§7请选择操作:")
.button("§a§l出售物品\n§7将物品转换为 EMC")
.button("§b§l购买物品\n§7使用 EMC 获取已学习的物品")
.button("§c关闭")
.validResultHandler(response -> {
// 主菜单不需要 session 检查(顶层入口), 但子菜单需要
if (!player.isOnline()) return;
switch (response.clickedButtonId()) {
case 0 -> SellEntryForm.open(player);
case 1 -> BuyListForm.open(player, 0, "");
case 2 -> { /* 关闭 */ }
}
})
.build();

BedrockFormUtil.sendForm(player, form);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.Little_100.projecte.bedrock.transmutation.buy;

import org.Little_100.projecte.ProjectE;
import org.Little_100.projecte.bedrock.BedrockFormUtil;
import org.Little_100.projecte.managers.EmcManager;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.geysermc.cumulus.form.CustomForm;

/**
* 自定义购买数量。
*
* 防刷:
* - slider 上限受 MAX_BUY_AMOUNT (EmcManager.MAX_BUY_AMOUNT=576) 和 maxAffordable 双重约束
* - 回调接收数量后再 clamp 一次, 防止客户端恶意伪造超限值
* - EMC 扣除最终仍在 EmcManager.buyItem 执行, 那里还有一次完整校验
*/
public final class BuyAmountForm {

private static final int IDX_SLIDER = 1;

private BuyAmountForm() {}

public static void open(Player player, String itemKey, int returnPage, String returnFilter) {
if (player == null || !player.isOnline()) return;
ProjectE plugin = ProjectE.getInstance();

ItemStack sample = plugin.getItemStackFromKey(itemKey);
if (sample == null) {
player.sendMessage("§c物品不可用");
BuyListForm.open(player, returnPage, returnFilter);
return;
}

long unitEmc = plugin.getEmcManager().getEmc(itemKey);
long playerEmc = plugin.getDatabaseManager().getPlayerEmc(player.getUniqueId());

if (unitEmc <= 0) {
player.sendMessage("§c该物品无 EMC 值");
BuyListForm.open(player, returnPage, returnFilter);
return;
}

// 计算上限: min(MAX_BUY_AMOUNT, playerEmc/unitEmc, maxStack*9)
int maxAffordable = (int) Math.min(playerEmc / unitEmc, EmcManager.MAX_BUY_AMOUNT);
maxAffordable = Math.min(maxAffordable, sample.getMaxStackSize() * 9);

if (maxAffordable < 1) {
player.sendMessage("§cEMC 不足, 连 1 个都买不起。");
BuyChoiceForm.open(player, itemKey, returnPage, returnFilter);
return;
}

String itemName = BedrockFormUtil.getDisplayName(sample);
String labelText = "§f物品: §e" + itemName + "\n"
+ "§f单价: §e" + BedrockFormUtil.formatEmc(unitEmc) + " EMC\n"
+ "§f你的 EMC: §e" + BedrockFormUtil.formatEmc(playerEmc) + "\n"
+ "§f最多可买: §e" + maxAffordable + " §f个\n\n"
+ "§7拖动滑块选择数量:";

final int max = maxAffordable;
CustomForm form = CustomForm.builder()
.title("§b§l购买 " + itemName)
.label(labelText)
.slider("数量", 1, max, 1, 1)
.validResultHandler(response -> {
if (!player.isOnline()) return;

int amount = (int) response.asSlider(IDX_SLIDER);
// [防御]: clamp, 防客户端发送超限值
if (amount < 1) amount = 1;
if (amount > max) amount = max;
if (amount > EmcManager.MAX_BUY_AMOUNT) amount = EmcManager.MAX_BUY_AMOUNT;

BuyConfirmForm.openConfirm(player, itemKey, amount, returnPage, returnFilter);
})
.closedOrInvalidResultHandler(() -> {
if (player.isOnline()) {
BuyChoiceForm.open(player, itemKey, returnPage, returnFilter);
}
})
.build();

BedrockFormUtil.sendForm(player, form);
}
}
Loading