From 2732a9aee10c3590b6d74ab0b8c5e93ebb80ead8 Mon Sep 17 00:00:00 2001 From: GaLicn <133291877+GaLicn@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:31:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A0=E7=BA=BF=E6=94=B6=E5=8F=91=E5=99=A8?= =?UTF-8?q?=E7=BB=91=E5=AE=9Aftbteams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + .../ae/items/ChannelCardItem.java | 96 +++++++- .../client/ChannelCardClientHandler.java | 55 +++++ .../wireless/WirelessTransceiverBlock.java | 76 ++++-- .../WirelessTransceiverBlockEntity.java | 60 +++++ .../com/extendedae_plus/init/ModNetwork.java | 4 + ...relessTransceiverJadePluginComponents.java | 33 +++ .../jade/WirelessTransceiverProvider.java | 39 +++- .../network/ChannelCardBindPacket.java | 83 +++++++ .../util/WirelessTeamUtil.java | 221 ++++++++++++++++++ .../wireless/WirelessMasterLink.java | 17 +- .../wireless/WirelessMasterRegistry.java | 56 ++++- .../wireless/WirelessSlaveLink.java | 13 +- .../assets/extendedae_plus/lang/en_us.json | 19 +- .../assets/extendedae_plus/lang/zh_cn.json | 19 +- 15 files changed, 761 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/extendedae_plus/client/ChannelCardClientHandler.java create mode 100644 src/main/java/com/extendedae_plus/network/ChannelCardBindPacket.java create mode 100644 src/main/java/com/extendedae_plus/util/WirelessTeamUtil.java diff --git a/build.gradle b/build.gradle index 30753b9..452518a 100644 --- a/build.gradle +++ b/build.gradle @@ -171,6 +171,11 @@ dependencies { //geckolib runtimeOnly "curse.maven:geckolib-388172:7009924" + //ftbteams + runtimeOnly "curse.maven:ftb-teams-forge-404468:6930910" + runtimeOnly "curse.maven:ftb-library-forge-404465:7029003" + runtimeOnly "curse.maven:architectury-api-419699:5786327" + runtimeOnly fileTree(dir: 'libs', includes: ['*.jar']) diff --git a/src/main/java/com/extendedae_plus/ae/items/ChannelCardItem.java b/src/main/java/com/extendedae_plus/ae/items/ChannelCardItem.java index 6a922c4..4cfcf18 100644 --- a/src/main/java/com/extendedae_plus/ae/items/ChannelCardItem.java +++ b/src/main/java/com/extendedae_plus/ae/items/ChannelCardItem.java @@ -1,9 +1,11 @@ package com.extendedae_plus.ae.items; import appeng.items.materials.UpgradeCardItem; +import com.extendedae_plus.util.WirelessTeamUtil; import net.minecraft.core.component.DataComponents; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -15,13 +17,20 @@ import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.UUID; /** - * 频道卡(MVP):仅存储一个 long 类型的频道号到 NBT:"channel"。 + * 频道卡:存储频道号、所有者UUID和团队信息 + * - 右键空气:增加频道号 + * - 潜行右键空气:减少频道号 + * - 潜行左键空气:写入/清除玩家UUID和团队信息(通过网络包处理) + * - 潜行左键收发器:将频道卡的所有者信息写入收发器 * 继承 AE2 的 UpgradeCardItem 以复用升级卡判定与提示框架。 */ public class ChannelCardItem extends UpgradeCardItem { public static final String TAG_CHANNEL = "channel"; + public static final String TAG_OWNER_UUID = "ownerUUID"; + public static final String TAG_TEAM_NAME = "teamName"; // 用于显示 public ChannelCardItem(Item.Properties properties) { super(properties); @@ -38,29 +47,110 @@ public class ChannelCardItem extends UpgradeCardItem { return tag.contains(TAG_CHANNEL) ? tag.getLong(TAG_CHANNEL) : 0L; } + /** + * 设置频道卡的所有者UUID + */ + public static void setOwnerUUID(ItemStack stack, UUID ownerUUID) { + CompoundTag tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + tag.putUUID(TAG_OWNER_UUID, ownerUUID); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } + + /** + * 获取频道卡的所有者UUID + */ + @Nullable + public static UUID getOwnerUUID(ItemStack stack) { + CompoundTag tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + return tag.hasUUID(TAG_OWNER_UUID) ? tag.getUUID(TAG_OWNER_UUID) : null; + } + + /** + * 设置团队名称(用于显示) + */ + public static void setTeamName(ItemStack stack, String teamName) { + CompoundTag tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + tag.putString(TAG_TEAM_NAME, teamName); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } + + /** + * 获取团队名称 + */ + @Nullable + public static String getTeamName(ItemStack stack) { + CompoundTag tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + return tag.contains(TAG_TEAM_NAME) ? tag.getString(TAG_TEAM_NAME) : null; + } + + /** + * 清除所有者信息 + */ + public static void clearOwner(ItemStack stack) { + CompoundTag tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + tag.remove(TAG_OWNER_UUID); + tag.remove(TAG_TEAM_NAME); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } + @Override public void appendHoverText(ItemStack stack, Item.TooltipContext context, List lines, TooltipFlag flag) { super.appendHoverText(stack, context, lines, flag); + + // 显示频道 long ch = getChannel(stack); if (ch == 0L) { lines.add(Component.translatable("item.extendedae_plus.channel_card.channel.unset")); } else { lines.add(Component.translatable("item.extendedae_plus.channel_card.channel", ch)); } + + // 显示所有者信息 + UUID ownerUUID = getOwnerUUID(stack); + String teamName = getTeamName(stack); + + if (ownerUUID != null) { + if (teamName != null && !teamName.isEmpty()) { + lines.add(Component.translatable("item.extendedae_plus.channel_card.owner.team", teamName)); + } else { + lines.add(Component.translatable("item.extendedae_plus.channel_card.owner.player", ownerUUID.toString().substring(0, 8))); + } + } else { + lines.add(Component.translatable("item.extendedae_plus.channel_card.owner.unset")); + } } @Override public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { ItemStack stack = player.getItemInHand(hand); + if (!level.isClientSide) { long ch = getChannel(stack); - boolean dec = player.isShiftKeyDown(); - long next = dec ? Math.max(0L, ch - 1L) : ch + 1L; + long next; + + if (player.isShiftKeyDown()) { + // 潜行右键:减少频率 + next = Math.max(0L, ch - 1L); + } else { + // 普通右键:增加频率 + next = ch + 1L; + } + if (next != ch) { setChannel(stack, next); player.displayClientMessage(Component.translatable("item.extendedae_plus.channel_card.set", next), true); } } + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide); } + + @Override + public boolean onLeftClickEntity(ItemStack stack, Player player, net.minecraft.world.entity.Entity entity) { + // 左键实体时不做任何事,避免伤害实体 + if (player.isShiftKeyDown()) { + return true; // 取消默认行为 + } + return super.onLeftClickEntity(stack, player, entity); + } } diff --git a/src/main/java/com/extendedae_plus/client/ChannelCardClientHandler.java b/src/main/java/com/extendedae_plus/client/ChannelCardClientHandler.java new file mode 100644 index 0000000..10cb2c3 --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/ChannelCardClientHandler.java @@ -0,0 +1,55 @@ +package com.extendedae_plus.client; + +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.init.ModItems; +import com.extendedae_plus.network.ChannelCardBindPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.network.PacketDistributor; + +/** + * 频道卡客户端事件处理器 + * 处理左键空气事件并发送网络包到服务端 + */ +@EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT) +public class ChannelCardClientHandler { + + /** + * 左键空气事件(仅客户端) + */ + @SubscribeEvent + public static void onLeftClickEmpty(InputEvent.InteractionKeyMappingTriggered event) { + // 只处理左键空气(attack模式) + if (!event.isAttack()) { + return; + } + + // 获取客户端玩家 + Player player = Minecraft.getInstance().player; + if (player == null) { + return; + } + + // 只处理潜行 + if (!player.isShiftKeyDown()) { + return; + } + + // 检查是否手持频道卡 + InteractionHand hand = event.getHand(); + ItemStack stack = player.getItemInHand(hand); + if (stack.getItem() != ModItems.CHANNEL_CARD.get()) { + return; + } + + // 发送网络包到服务端 + PacketDistributor.sendToServer(new ChannelCardBindPacket(hand)); + } +} + diff --git a/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlock.java b/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlock.java index 92d20ca..6086366 100644 --- a/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlock.java +++ b/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlock.java @@ -1,12 +1,16 @@ package com.extendedae_plus.content.wireless; +import com.extendedae_plus.ae.items.ChannelCardItem; import com.extendedae_plus.init.ModBlockEntities; +import com.extendedae_plus.init.ModItems; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; @@ -16,6 +20,9 @@ import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; public class WirelessTransceiverBlock extends Block implements EntityBlock { public WirelessTransceiverBlock(Properties props) { @@ -28,28 +35,71 @@ public class WirelessTransceiverBlock extends Block implements EntityBlock { } @Override - public void attack(BlockState state, Level level, BlockPos pos, Player player) { - // 潜行左键:减频(-1 或 -10) - if (!level.isClientSide && player.isShiftKeyDown()) { + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + if (!level.isClientSide && placer instanceof Player player) { BlockEntity be = level.getBlockEntity(pos); if (be instanceof WirelessTransceiverBlockEntity te) { - if (te.isLocked()) { - player.displayClientMessage(Component.literal("收发器已锁定,无法修改频道"), true); + te.setPlacerId(player.getUUID(), player.getName().getString()); + } + } + } + + @Override + public void attack(BlockState state, Level level, BlockPos pos, Player player) { + if (!level.isClientSide) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof WirelessTransceiverBlockEntity te) { + ItemStack mainHand = player.getMainHandItem(); + + // 潜行左键频道卡:写入频道卡信息到收发器 + if (player.isShiftKeyDown() && mainHand.getItem() == ModItems.CHANNEL_CARD.get()) { + handleChannelCardBinding(te, mainHand, player); super.attack(state, level, pos, player); return; } - int step = 1; - if (player.getMainHandItem().is(Items.REDSTONE_TORCH)) step = 10; - if (player.getMainHandItem().is(Items.STICK)) step = 10; - long f = te.getFrequency(); - f -= step; - if (f < 0) f = 0; - te.setFrequency(f); - player.displayClientMessage(Component.literal("频道:" + te.getFrequency()), true); + + // 潜行左键(其他物品):减频(-1 或 -10) + if (player.isShiftKeyDown()) { + if (te.isLocked()) { + player.displayClientMessage(Component.literal("收发器已锁定,无法修改频道"), true); + super.attack(state, level, pos, player); + return; + } + int step = 1; + if (mainHand.is(Items.REDSTONE_TORCH)) step = 10; + if (mainHand.is(Items.STICK)) step = 10; + long f = te.getFrequency(); + f -= step; + if (f < 0) f = 0; + te.setFrequency(f); + player.displayClientMessage(Component.literal("频道:" + te.getFrequency()), true); + } } } super.attack(state, level, pos, player); } + + /** + * 处理频道卡绑定到收发器 + */ + private void handleChannelCardBinding(WirelessTransceiverBlockEntity te, ItemStack channelCard, Player player) { + UUID cardOwner = ChannelCardItem.getOwnerUUID(channelCard); + + if (cardOwner != null) { + // 写入频道卡的所有者到收发器 + String teamName = ChannelCardItem.getTeamName(channelCard); + te.setPlacerId(cardOwner, teamName); + player.displayClientMessage( + Component.literal("已将收发器绑定至:" + (teamName != null ? teamName : cardOwner.toString().substring(0, 8))), + true + ); + } else { + // 频道卡未绑定所有者,使用当前玩家 + te.setPlacerId(player.getUUID(), player.getName().getString()); + player.displayClientMessage(Component.literal("频道卡未绑定,已使用当前玩家"), true); + } + } // 1.21+: 拆分为 useItemOn 与 useWithoutItem @Override diff --git a/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlockEntity.java b/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlockEntity.java index b4dd71d..37a38b2 100644 --- a/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlockEntity.java +++ b/src/main/java/com/extendedae_plus/content/wireless/WirelessTransceiverBlockEntity.java @@ -19,6 +19,7 @@ import org.jetbrains.annotations.Nullable; import java.util.EnumSet; import java.util.Objects; +import java.util.UUID; /** * 无线收发器方块实体(骨架): @@ -34,6 +35,11 @@ public class WirelessTransceiverBlockEntity extends AEBaseBlockEntity implements private long frequency = 1L; private boolean masterMode = false; private boolean locked = false; + + @Nullable + private UUID placerId; // 放置者UUID,用于队伍隔离 + @Nullable + private String placerName; // 放置者名称,用于显示 private WirelessMasterLink masterLink; private WirelessSlaveLink slaveLink; @@ -97,6 +103,43 @@ public class WirelessTransceiverBlockEntity extends AEBaseBlockEntity implements } /* ===================== 公共方法(交互调用) ===================== */ + + /** + * 设置放置者UUID和名称(在方块放置时调用) + */ + public void setPlacerId(@Nullable UUID placerId, @Nullable String placerName) { + if (this.placerId != null && !this.placerId.equals(placerId)) { + // 如果所有者改变,需要重新注册 + if (this.masterMode) { + masterLink.onUnloadOrRemove(); + } else { + slaveLink.onUnloadOrRemove(); + } + } + this.placerId = placerId; + this.placerName = placerName; + this.masterLink.setPlacerId(placerId); + this.slaveLink.setPlacerId(placerId); + setChanged(); + } + + /** + * 仅设置UUID(兼容旧代码) + */ + public void setPlacerId(@Nullable UUID placerId) { + setPlacerId(placerId, null); + } + + @Nullable + public UUID getPlacerId() { + return placerId; + } + + @Nullable + public String getPlacerName() { + return placerName; + } + public long getFrequency() { return frequency; } @@ -193,6 +236,12 @@ public class WirelessTransceiverBlockEntity extends AEBaseBlockEntity implements tag.putLong("frequency", frequency); tag.putBoolean("master", masterMode); tag.putBoolean("locked", locked); + if (placerId != null) { + tag.putUUID("placerId", placerId); + } + if (placerName != null) { + tag.putString("placerName", placerName); + } if (managedNode != null) { managedNode.saveToNBT(tag); } @@ -204,6 +253,17 @@ public class WirelessTransceiverBlockEntity extends AEBaseBlockEntity implements this.frequency = tag.getLong("frequency"); this.masterMode = tag.getBoolean("master"); this.locked = tag.getBoolean("locked"); + + if (tag.hasUUID("placerId")) { + this.placerId = tag.getUUID("placerId"); + this.masterLink.setPlacerId(this.placerId); + this.slaveLink.setPlacerId(this.placerId); + } + + if (tag.contains("placerName")) { + this.placerName = tag.getString("placerName"); + } + if (managedNode != null) { managedNode.loadFromNBT(tag); } diff --git a/src/main/java/com/extendedae_plus/init/ModNetwork.java b/src/main/java/com/extendedae_plus/init/ModNetwork.java index 4d618f5..89e87ac 100644 --- a/src/main/java/com/extendedae_plus/init/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/init/ModNetwork.java @@ -34,5 +34,9 @@ public class ModNetwork { registrar.playToServer(com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket.TYPE, com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket.STREAM_CODEC, com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket::handle); + // 频道卡绑定 + registrar.playToServer(com.extendedae_plus.network.ChannelCardBindPacket.TYPE, + com.extendedae_plus.network.ChannelCardBindPacket.STREAM_CODEC, + com.extendedae_plus.network.ChannelCardBindPacket::handle); } } diff --git a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java index 22095a6..e8c2bfe 100644 --- a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java +++ b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java @@ -66,6 +66,39 @@ public enum WirelessTransceiverJadePluginComponents implements IBlockComponentPr tooltip.add(Component.literal((usable ? "设备在线" : "设备离线"))); } } + }, + OWNER("wt_owner") { + @Override + protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) { + if (data.contains("ownerName")) { + String ownerName = data.getString("ownerName"); + tooltip.add(Component.translatable("extendedae_plus.tooltip.owner", ownerName)); + } else if (data.hasUUID("placerId")) { + // 有placerId但没有名称,显示UUID + java.util.UUID placerId = data.getUUID("placerId"); + tooltip.add(Component.translatable("extendedae_plus.tooltip.owner", placerId.toString().substring(0, 8) + "...")); + } else { + // 没有所有者信息(公共收发器) + tooltip.add(Component.translatable("extendedae_plus.tooltip.owner.public")); + } + } + }, + CHANNELS("wt_channels") { + @Override + protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) { + if (data.contains("usedChannels") && data.contains("maxChannels")) { + int usedChannels = data.getInt("usedChannels"); + int maxChannels = data.getInt("maxChannels"); + // 参考AE2的显示方式 + if (maxChannels <= 0) { + // 无限频道或未设置 + tooltip.add(Component.translatable("extendedae_plus.tooltip.channels", usedChannels)); + } else { + // 显示 "已使用/最大" + tooltip.add(Component.translatable("extendedae_plus.tooltip.channels_of", usedChannels, maxChannels)); + } + } + } }; private final ResourceLocation uid; diff --git a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java index acf11d3..e5e0d3e 100644 --- a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java +++ b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java @@ -7,6 +7,7 @@ import com.extendedae_plus.wireless.IWirelessEndpoint; import com.extendedae_plus.wireless.WirelessMasterRegistry; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import snownee.jade.api.BlockAccessor; import snownee.jade.api.IServerDataProvider; @@ -28,6 +29,19 @@ public enum WirelessTransceiverProvider implements IServerDataProvider TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(ExtendedAEPlus.MODID, "channel_card_bind")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeEnum(pkt.hand), + buf -> new ChannelCardBindPacket(buf.readEnum(InteractionHand.class)) + ); + + private final InteractionHand hand; + + public ChannelCardBindPacket(InteractionHand hand) { + this.hand = hand; + } + + @Override + public Type type() { + return TYPE; + } + + public static void handle(final ChannelCardBindPacket msg, final IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + + ItemStack stack = player.getItemInHand(msg.hand); + if (stack.getItem() != ModItems.CHANNEL_CARD.get()) { + return; + } + + ServerLevel level = player.serverLevel(); + UUID currentOwner = ChannelCardItem.getOwnerUUID(stack); + + if (currentOwner != null) { + // 已有所有者,清除 + ChannelCardItem.clearOwner(stack); + player.displayClientMessage( + Component.translatable("item.extendedae_plus.channel_card.owner.cleared"), + true + ); + } else { + // 写入当前玩家的UUID和团队信息 + UUID playerUUID = player.getUUID(); + ChannelCardItem.setOwnerUUID(stack, playerUUID); + + // 获取团队名称用于显示 + Component teamName = WirelessTeamUtil.getNetworkOwnerName(level, playerUUID); + ChannelCardItem.setTeamName(stack, teamName.getString()); + + player.displayClientMessage( + Component.translatable("item.extendedae_plus.channel_card.owner.bound", teamName), + true + ); + } + }); + } +} + diff --git a/src/main/java/com/extendedae_plus/util/WirelessTeamUtil.java b/src/main/java/com/extendedae_plus/util/WirelessTeamUtil.java new file mode 100644 index 0000000..bf0108c --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/WirelessTeamUtil.java @@ -0,0 +1,221 @@ +package com.extendedae_plus.util; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.fml.ModList; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * 无线收发器队伍工具类 + * 实现FTBTeams软依赖:有FTBTeams时使用队伍UUID,没有时使用玩家UUID + */ +public class WirelessTeamUtil { + + private static final boolean FTB_TEAMS_LOADED = ModList.get().isLoaded("ftbteams"); + + /** + * 获取用于无线网络隔离的UUID + * - 如果安装了FTBTeams且玩家在队伍中,返回队伍UUID(同队玩家共享) + * - 否则返回玩家自己的UUID(独立网络) + * + * @param level 服务端世界 + * @param playerUUID 玩家UUID + * @return 网络所有者UUID + */ + public static UUID getNetworkOwnerUUID(@Nullable ServerLevel level, UUID playerUUID) { + if (playerUUID == null) { + return null; + } + + if (!FTB_TEAMS_LOADED) { + return playerUUID; + } + + if (level == null) { + return playerUUID; + } + + try { + return getTeamUUID(level, playerUUID); + } catch (Exception e) { + // 如果FTBTeams API调用失败,回退到玩家UUID + return playerUUID; + } + } + + /** + * 获取网络所有者的显示名称(用于UI显示) + * + * @param level 服务端世界 + * @param playerUUID 玩家UUID + * @return 显示名称 + */ + public static Component getNetworkOwnerName(@Nullable ServerLevel level, UUID playerUUID) { + if (FTB_TEAMS_LOADED && level != null) { + try { + return getTeamName(level, playerUUID); + } catch (Exception ignored) { + } + } + + // 尝试获取玩家名称 + if (level != null) { + ServerPlayer player = level.getServer().getPlayerList().getPlayer(playerUUID); + if (player != null) { + return player.getName(); + } + } + + return Component.literal(playerUUID.toString()); + } + + /** + * 检查网络所有者是否有效(玩家在线或队伍存在) + * + * @param level 服务端世界 + * @param playerUUID 玩家UUID + * @return 是否有效 + */ + public static boolean hasNetworkOwner(@Nullable ServerLevel level, UUID playerUUID) { + if (FTB_TEAMS_LOADED && level != null) { + try { + return hasTeamOwner(level, playerUUID); + } catch (Exception ignored) { + } + } + + // 检查玩家是否在线 + if (level != null) { + return level.getServer().getPlayerList().getPlayer(playerUUID) != null; + } + + return false; + } + + // ==================== FTBTeams 集成(通过反射调用避免硬依赖)==================== + + private static UUID getTeamUUID(ServerLevel level, UUID playerUUID) { + try { + // 使用FTBTeams API + var apiClass = Class.forName("dev.ftb.mods.ftbteams.api.FTBTeamsAPI"); + var api = apiClass.getMethod("api").invoke(null); // 静态方法,返回API实例 + + // 检查Manager是否已加载(在api实例上调用) + Boolean isLoaded = (Boolean) api.getClass().getMethod("isManagerLoaded").invoke(api); + + if (!isLoaded) { + return playerUUID; + } + + var getManager = api.getClass().getMethod("getManager").invoke(api); + + if (getManager == null) { + return playerUUID; + } + + var managerClass = getManager.getClass(); + var getTeamForPlayer = managerClass.getMethod("getTeamForPlayerID", UUID.class); + var teamOptional = getTeamForPlayer.invoke(getManager, playerUUID); + + if (teamOptional != null) { + var optionalClass = teamOptional.getClass(); + var isPresent = (boolean) optionalClass.getMethod("isPresent").invoke(teamOptional); + + if (isPresent) { + var team = optionalClass.getMethod("get").invoke(teamOptional); + var teamClass = team.getClass(); + return (UUID) teamClass.getMethod("getTeamId").invoke(team); + } + } + } catch (Exception e) { + // 反射调用失败,回退 + } + + return playerUUID; + } + + private static Component getTeamName(ServerLevel level, UUID playerUUID) { + try { + var apiClass = Class.forName("dev.ftb.mods.ftbteams.api.FTBTeamsAPI"); + var api = apiClass.getMethod("api").invoke(null); + + // 检查Manager是否已加载 + Boolean isLoaded = (Boolean) api.getClass().getMethod("isManagerLoaded").invoke(api); + if (!isLoaded) { + // Manager未加载,回退到玩家名称 + ServerPlayer player = level.getServer().getPlayerList().getPlayer(playerUUID); + if (player != null) { + return player.getName(); + } + return Component.literal(playerUUID.toString()); + } + + var getManager = api.getClass().getMethod("getManager").invoke(api); + + if (getManager == null) { + return Component.literal(playerUUID.toString()); + } + + var managerClass = getManager.getClass(); + var getTeamForPlayer = managerClass.getMethod("getTeamForPlayerID", UUID.class); + var teamOptional = getTeamForPlayer.invoke(getManager, playerUUID); + + if (teamOptional != null) { + var optionalClass = teamOptional.getClass(); + var isPresent = (boolean) optionalClass.getMethod("isPresent").invoke(teamOptional); + + if (isPresent) { + var team = optionalClass.getMethod("get").invoke(teamOptional); + var teamClass = team.getClass(); + return (Component) teamClass.getMethod("getName").invoke(team); + } + } + } catch (Exception e) { + // 反射调用失败,回退 + } + + // 回退到玩家名称 + ServerPlayer player = level.getServer().getPlayerList().getPlayer(playerUUID); + if (player != null) { + return player.getName(); + } + + return Component.literal(playerUUID.toString()); + } + + private static boolean hasTeamOwner(ServerLevel level, UUID playerUUID) { + try { + var apiClass = Class.forName("dev.ftb.mods.ftbteams.api.FTBTeamsAPI"); + var api = apiClass.getMethod("api").invoke(null); + + // 检查Manager是否已加载 + Boolean isLoaded = (Boolean) api.getClass().getMethod("isManagerLoaded").invoke(api); + if (!isLoaded) { + return level.getServer().getPlayerList().getPlayer(playerUUID) != null; + } + + var getManager = api.getClass().getMethod("getManager").invoke(api); + + if (getManager == null) { + return level.getServer().getPlayerList().getPlayer(playerUUID) != null; + } + + var managerClass = getManager.getClass(); + var getTeamForPlayer = managerClass.getMethod("getTeamForPlayerID", UUID.class); + var teamOptional = getTeamForPlayer.invoke(getManager, playerUUID); + + if (teamOptional != null) { + var optionalClass = teamOptional.getClass(); + return (boolean) optionalClass.getMethod("isPresent").invoke(teamOptional); + } + } catch (Exception e) { + // 反射调用失败,回退 + } + + return level.getServer().getPlayerList().getPlayer(playerUUID) != null; + } +} + diff --git a/src/main/java/com/extendedae_plus/wireless/WirelessMasterLink.java b/src/main/java/com/extendedae_plus/wireless/WirelessMasterLink.java index 4024b49..6bf58ec 100644 --- a/src/main/java/com/extendedae_plus/wireless/WirelessMasterLink.java +++ b/src/main/java/com/extendedae_plus/wireless/WirelessMasterLink.java @@ -1,6 +1,9 @@ package com.extendedae_plus.wireless; import net.minecraft.server.level.ServerLevel; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; /** * 主收发器端逻辑:负责在频率变化/加载时向注册中心登记唯一主端,卸载时反注册。 @@ -10,10 +13,16 @@ public class WirelessMasterLink { private final IWirelessEndpoint host; private long frequency; // 0 为未设置 private boolean registered; + @Nullable + private UUID placerId; // 放置者UUID public WirelessMasterLink(IWirelessEndpoint host) { this.host = host; } + + public void setPlacerId(@Nullable UUID placerId) { + this.placerId = placerId; + } public long getFrequency() { return frequency; } @@ -27,7 +36,7 @@ public class WirelessMasterLink { } // 频率未变的情况下也要校正注册状态: - // - 当从“从端”切回“主端”时,registered 可能为 false,需要重新注册; + // - 当从"从端"切回"主端"时,registered 可能为 false,需要重新注册; // - 当频率为 0 或端点被移除时,确保处于未注册。 if (frequency != 0L && !host.isEndpointRemoved()) { if (!registered) { @@ -43,7 +52,8 @@ public class WirelessMasterLink { public boolean register() { ServerLevel level = host.getServerLevel(); if (level == null || frequency == 0L) return false; - boolean ok = WirelessMasterRegistry.register(level, frequency, host); + // placerId可以为null(公共收发器模式) + boolean ok = WirelessMasterRegistry.register(level, frequency, placerId, host); this.registered = ok; return ok; } @@ -51,7 +61,8 @@ public class WirelessMasterLink { public void unregister() { ServerLevel level = host.getServerLevel(); if (!registered || level == null || frequency == 0L) return; - WirelessMasterRegistry.unregister(level, frequency, host); + // placerId可以为null(公共收发器模式) + WirelessMasterRegistry.unregister(level, frequency, placerId, host); registered = false; } diff --git a/src/main/java/com/extendedae_plus/wireless/WirelessMasterRegistry.java b/src/main/java/com/extendedae_plus/wireless/WirelessMasterRegistry.java index 408bab0..7d26771 100644 --- a/src/main/java/com/extendedae_plus/wireless/WirelessMasterRegistry.java +++ b/src/main/java/com/extendedae_plus/wireless/WirelessMasterRegistry.java @@ -1,43 +1,68 @@ package com.extendedae_plus.wireless; import com.extendedae_plus.config.ModConfigs; +import com.extendedae_plus.util.WirelessTeamUtil; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.UUID; /** - * 无线主端注册中心:按 维度 + 频率 唯一注册一个主收发器端点。 + * 无线主端注册中心:按 维度 + 频率 + 所有者 唯一注册一个主收发器端点。 * 从端通过本注册中心按频率查找主端,实现一对多连接。 + * 所有者隔离:有FTBTeams时同队共享,没有时每个玩家独立。 + * 公共模式:placerId为null时使用公共UUID,所有人都能访问(向下兼容旧版本)。 */ public final class WirelessMasterRegistry { private WirelessMasterRegistry() {} private static final Map> MASTERS = new HashMap<>(); + + /** + * 公共收发器UUID(用于没有设置所有者的收发器) + * 所有placerId为null的收发器都使用这个UUID,实现公共访问 + */ + public static final UUID PUBLIC_NETWORK_UUID = new UUID(0, 0); - public static synchronized boolean register(ServerLevel level, long frequency, IWirelessEndpoint endpoint) { + public static synchronized boolean register(ServerLevel level, long frequency, @Nullable UUID placerId, IWirelessEndpoint endpoint) { Objects.requireNonNull(level, "level"); Objects.requireNonNull(endpoint, "endpoint"); if (frequency == 0L) return false; - final Key key = new Key(useGlobal() ? null : level.dimension(), frequency); + + // 获取网络所有者UUID + // placerId为null时使用公共UUID(向下兼容旧版本收发器) + UUID ownerUUID = placerId != null + ? WirelessTeamUtil.getNetworkOwnerUUID(level, placerId) + : PUBLIC_NETWORK_UUID; + + final Key key = new Key(useGlobal() ? null : level.dimension(), frequency, ownerUUID); + cleanupIfCleared(key); var existing = MASTERS.get(key); var existingVal = existing == null ? null : existing.get(); if (existingVal != null && !existingVal.isEndpointRemoved()) { - // 同维度同频率已经有主端 + // 同维度同频率同所有者已经有主端 return false; } MASTERS.put(key, new WeakReference<>(endpoint)); return true; } - public static synchronized void unregister(ServerLevel level, long frequency, IWirelessEndpoint endpoint) { + public static synchronized void unregister(ServerLevel level, long frequency, @Nullable UUID placerId, IWirelessEndpoint endpoint) { if (frequency == 0L || level == null) return; - final Key key = new Key(useGlobal() ? null : level.dimension(), frequency); + + UUID ownerUUID = placerId != null + ? WirelessTeamUtil.getNetworkOwnerUUID(level, placerId) + : PUBLIC_NETWORK_UUID; + + final Key key = new Key(useGlobal() ? null : level.dimension(), frequency, ownerUUID); + var ref = MASTERS.get(key); if (ref != null) { var cur = ref.get(); @@ -47,9 +72,15 @@ public final class WirelessMasterRegistry { } } - public static synchronized IWirelessEndpoint get(ServerLevel level, long frequency) { + public static synchronized IWirelessEndpoint get(ServerLevel level, long frequency, @Nullable UUID placerId) { if (frequency == 0L || level == null) return null; - final Key key = new Key(useGlobal() ? null : level.dimension(), frequency); + + UUID ownerUUID = placerId != null + ? WirelessTeamUtil.getNetworkOwnerUUID(level, placerId) + : PUBLIC_NETWORK_UUID; + + final Key key = new Key(useGlobal() ? null : level.dimension(), frequency, ownerUUID); + cleanupIfCleared(key); var ref = MASTERS.get(key); return ref == null ? null : ref.get(); @@ -66,9 +97,12 @@ public final class WirelessMasterRegistry { return ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get(); } - private record Key(ResourceKey dim, long freq) { - @Override public String toString() { - return (dim == null ? "*" : dim.location().toString()) + "#" + freq; + private record Key(@Nullable ResourceKey dim, long freq, UUID owner) { + @Override + public String toString() { + return (dim == null ? "*" : dim.location().toString()) + + "#" + freq + + "@" + owner; } } } diff --git a/src/main/java/com/extendedae_plus/wireless/WirelessSlaveLink.java b/src/main/java/com/extendedae_plus/wireless/WirelessSlaveLink.java index 872e461..9a6c177 100644 --- a/src/main/java/com/extendedae_plus/wireless/WirelessSlaveLink.java +++ b/src/main/java/com/extendedae_plus/wireless/WirelessSlaveLink.java @@ -7,18 +7,22 @@ import appeng.me.service.helpers.ConnectionWrapper; import com.extendedae_plus.config.ModConfigs; import com.extendedae_plus.util.ExtendedAELogger; import net.minecraft.server.level.ServerLevel; +import org.jetbrains.annotations.Nullable; import java.util.Objects; +import java.util.UUID; /** * 从收发器连接器: * - 通过频率查找同维度主收发器; * - 校验距离(<= ModConfigs.WIRELESS_MAX_RANGE); - * - 动态创建/销毁 AE2 连接(GridConnection),实现“一主多从”。 + * - 动态创建/销毁 AE2 连接(GridConnection),实现"一主多从"。 */ public class WirelessSlaveLink { private final IWirelessEndpoint host; private long frequency; // 0 未设置 + @Nullable + private UUID placerId; // 放置者UUID private ConnectionWrapper connection = new ConnectionWrapper(null); private boolean shutdown = true; @@ -27,6 +31,10 @@ public class WirelessSlaveLink { public WirelessSlaveLink(IWirelessEndpoint host) { this.host = Objects.requireNonNull(host); } + + public void setPlacerId(@Nullable UUID placerId) { + this.placerId = placerId; + } public void setFrequency(long frequency) { if (this.frequency != frequency) { @@ -62,7 +70,8 @@ public class WirelessSlaveLink { return; } - IWirelessEndpoint master = WirelessMasterRegistry.get(level, frequency); + // placerId可以为null(公共收发器模式) + IWirelessEndpoint master = WirelessMasterRegistry.get(level, frequency, placerId); shutdown = false; distance = 0.0D; diff --git a/src/main/resources/assets/extendedae_plus/lang/en_us.json b/src/main/resources/assets/extendedae_plus/lang/en_us.json index bb670f4..160fea8 100644 --- a/src/main/resources/assets/extendedae_plus/lang/en_us.json +++ b/src/main/resources/assets/extendedae_plus/lang/en_us.json @@ -22,6 +22,11 @@ "item.extendedae_plus.channel_card.channel": "Channel: %d", "item.extendedae_plus.channel_card.channel.unset": "Channel: Unset", "item.extendedae_plus.channel_card.set": "Channel set to: %d", + "item.extendedae_plus.channel_card.owner.unset": "Owner: Unbound", + "item.extendedae_plus.channel_card.owner.team": "Owner: %s", + "item.extendedae_plus.channel_card.owner.player": "Owner: %s", + "item.extendedae_plus.channel_card.owner.bound": "Bound to: %s", + "item.extendedae_plus.channel_card.owner.cleared": "Binding cleared", "item.extendedae_plus.entity_speed_card": "Entity Speed Card", "item.extendedae_plus.entity_speed_card.x2": "Entity Speed Card (x2)", "item.extendedae_plus.entity_speed_card.x4": "Entity Speed Card (x4)", @@ -103,5 +108,17 @@ "extendedae_plus.configuration.prioritizeDiskEnergy": "Prioritize FE energy from disk (requires Applied Flux)", "extendedae_plus.configuration.state_on": "On", - "extendedae_plus.configuration.state_off": "Off" + "extendedae_plus.configuration.state_off": "Off", + + "extendedae_plus.tooltip.owner": "Owner: %s", + "extendedae_plus.tooltip.owner.public": "Owner: Public", + "extendedae_plus.tooltip.channels": "Channels: %d", + "extendedae_plus.tooltip.channels_of": "Channels: %d / %d", + "extendedae_plus.tooltip.frequency": "Frequency: %d", + "extendedae_plus.tooltip.master_mode": "Mode: %s", + "extendedae_plus.tooltip.locked": "Status: %s", + + "group.pattern_provider.name": "Pattern Provider", + "group.storage.name": "Storage Bus", + "group.entity_ticker.name": "Entity Ticker" } \ No newline at end of file diff --git a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json index 012a707..840d62c 100644 --- a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json +++ b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json @@ -22,6 +22,11 @@ "item.extendedae_plus.channel_card.channel": "频道: %d", "item.extendedae_plus.channel_card.channel.unset": "频道: 未设置", "item.extendedae_plus.channel_card.set": "频道已设置为: %d", + "item.extendedae_plus.channel_card.owner.unset": "所有者: 未绑定", + "item.extendedae_plus.channel_card.owner.team": "所有者: %s", + "item.extendedae_plus.channel_card.owner.player": "所有者: %s", + "item.extendedae_plus.channel_card.owner.bound": "已绑定到: %s", + "item.extendedae_plus.channel_card.owner.cleared": "已清除绑定", "item.extendedae_plus.entity_speed_card": "实体加速卡", "item.extendedae_plus.entity_speed_card.x2": "实体加速卡 (x2)", "item.extendedae_plus.entity_speed_card.x4": "实体加速卡 (x4)", @@ -103,5 +108,17 @@ "extendedae_plus.configuration.prioritizeDiskEnergy": "优先从磁盘提取FE能量(仅当Applied Flux模组存在时生效)", "extendedae_plus.configuration.state_on": "开", - "extendedae_plus.configuration.state_off": "关" + "extendedae_plus.configuration.state_off": "关", + + "extendedae_plus.tooltip.owner": "所有者: %s", + "extendedae_plus.tooltip.owner.public": "所有者: 公共", + "extendedae_plus.tooltip.channels": "频道: %d", + "extendedae_plus.tooltip.channels_of": "频道: %d / %d", + "extendedae_plus.tooltip.frequency": "频率: %d", + "extendedae_plus.tooltip.master_mode": "模式: %s", + "extendedae_plus.tooltip.locked": "状态: %s", + + "group.pattern_provider.name": "样板供应器", + "group.storage.name": "存储总线", + "group.entity_ticker.name": "实体加速器" } \ No newline at end of file