UI cleanup, trade implementation via hotkey and via /trade player|yes|no

This commit is contained in:
trunksbomb
2026-03-24 22:31:25 -04:00
parent 31fd6c33ff
commit b32a13ab84
9 changed files with 272 additions and 37 deletions

View File

@@ -18,6 +18,13 @@ public final class TradeClientState {
return minecraft;
}
public static void showOrUpdate(TradeView view) {
if (minecraft == null) {
minecraft = Minecraft.getInstance();
}
showOrUpdate(minecraft, view);
}
public static void showOrUpdate(Minecraft minecraftInstance, TradeView view) {
minecraft = minecraftInstance;
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(view.sessionId())) {
@@ -32,6 +39,13 @@ public final class TradeClientState {
minecraft.setScreen(new TradeScreen(new TradeScreen.TradeMenu(0, minecraft.player.getInventory(), view), minecraft.player.getInventory(), Component.literal("Trade")));
}
public static void closeTrade(UUID sessionId, Component reason) {
if (minecraft == null) {
minecraft = Minecraft.getInstance();
}
closeTrade(minecraft, sessionId, reason);
}
public static void closeTrade(Minecraft minecraftInstance, UUID sessionId, Component reason) {
minecraft = minecraftInstance;
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(sessionId)) {

View File

@@ -31,9 +31,11 @@ public final class TradeCommand {
.then(Commands.literal("accept").executes(context -> acceptDebug(context.getSource())))
.then(Commands.literal("cancel").executes(context -> cancelDebug(context.getSource())))
.then(Commands.literal("close").executes(context -> closeDebug(context.getSource()))))
.then(Commands.literal("yes").executes(context -> respondTrade(context.getSource(), true)))
.then(Commands.literal("no").executes(context -> respondTrade(context.getSource(), false)))
.then(Commands.argument("player", StringArgumentType.word())
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
.executes(context -> startTrade(context.getSource(), StringArgumentType.getString(context, "player")))));
.executes(context -> requestTrade(context.getSource(), StringArgumentType.getString(context, "player")))));
}
private static CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestPlayers(CommandSourceStack source, SuggestionsBuilder builder) {
@@ -45,7 +47,7 @@ public final class TradeCommand {
return builder.buildFuture();
}
private static int startTrade(CommandSourceStack source, String targetName) {
private static int requestTrade(CommandSourceStack source, String targetName) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can start trades."));
return 0;
@@ -62,15 +64,25 @@ public final class TradeCommand {
return 0;
}
if (!TradeManager.get(source.getServer()).startTrade(player, target)) {
source.sendFailure(Component.literal("Trade could not be started. One of you is already trading."));
if (!TradeManager.get(source.getServer()).requestTrade(player, target)) {
return 0;
}
source.sendSuccess(() -> Component.literal("Trade opened with " + target.getGameProfile().getName() + "."), false);
return 1;
}
private static int respondTrade(CommandSourceStack source, boolean accept) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can answer trade requests."));
return 0;
}
boolean result = accept
? TradeManager.get(source.getServer()).acceptPendingTrade(player)
: TradeManager.get(source.getServer()).declinePendingTrade(player);
return result ? 1 : 0;
}
private static int initDebug(CommandSourceStack source) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can start debug trades."));

View File

@@ -1,18 +1,52 @@
package com.trunksbomb.trade.mod;
import com.mojang.blaze3d.platform.InputConstants;
import com.trunksbomb.trade.mod.client.TradeClientState;
import com.trunksbomb.trade.mod.network.TradeRequestPayload;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.network.PacketDistributor;
import org.lwjgl.glfw.GLFW;
@Mod(value = TradeMod.MODID, dist = Dist.CLIENT)
@EventBusSubscriber(modid = TradeMod.MODID, value = Dist.CLIENT)
public class TradeClientMod {
@SubscribeEvent
static void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> TradeClientState.init(Minecraft.getInstance()));
private static final KeyMapping REQUEST_TRADE = new KeyMapping(
"key.trade.request_trade",
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_G,
"key.categories.gameplay");
public TradeClientMod(IEventBus modEventBus) {
modEventBus.addListener(this::registerKeyMappings);
NeoForge.EVENT_BUS.addListener(this::onClientTick);
TradeClientState.init(Minecraft.getInstance());
}
private void registerKeyMappings(RegisterKeyMappingsEvent event) {
event.register(REQUEST_TRADE);
}
private void onClientTick(ClientTickEvent.Post event) {
Minecraft minecraft = Minecraft.getInstance();
while (REQUEST_TRADE.consumeClick()) {
if (minecraft.player == null || minecraft.screen != null) {
continue;
}
if (!(minecraft.crosshairPickEntity instanceof Player target) || target == minecraft.player) {
minecraft.player.displayClientMessage(Component.literal("Look at a player to send a trade request."), true);
continue;
}
PacketDistributor.sendToServer(new TradeRequestPayload(target.getId()));
}
}
}

View File

@@ -0,0 +1,24 @@
package com.trunksbomb.trade.mod;
import net.neoforged.neoforge.common.ModConfigSpec;
public final class TradeConfig {
public static final ModConfigSpec SPEC;
private static final ModConfigSpec.IntValue TRADE_COMMAND_PROXIMITY;
static {
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
builder.push("trade");
TRADE_COMMAND_PROXIMITY = builder
.comment("Maximum distance in blocks to initiate or accept a trade request. 0 disables the distance check.")
.defineInRange("tradeCommandProximity", 0, 0, 1024);
builder.pop();
SPEC = builder.build();
}
private TradeConfig() {}
public static int tradeCommandProximity() {
return TRADE_COMMAND_PROXIMITY.get();
}
}

View File

@@ -5,6 +5,7 @@ import com.trunksbomb.trade.mod.command.TradeCommand;
import com.trunksbomb.trade.mod.network.TradeNetworking;
import com.trunksbomb.trade.mod.trade.TradeManager;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.common.NeoForge;
@@ -18,9 +19,10 @@ public class TradeMod {
public static final String MODID = "trade";
public static final Logger LOGGER = LogUtils.getLogger();
public TradeMod(IEventBus modEventBus) {
public TradeMod(IEventBus modEventBus, ModContainer modContainer) {
modEventBus.addListener(this::commonSetup);
modEventBus.addListener(this::registerPayloads);
modContainer.registerConfig(net.neoforged.fml.config.ModConfig.Type.SERVER, TradeConfig.SPEC);
NeoForge.EVENT_BUS.addListener(this::registerCommands);
NeoForge.EVENT_BUS.addListener(this::onPlayerLogout);
NeoForge.EVENT_BUS.addListener(this::onItemPickup);

View File

@@ -1,8 +1,6 @@
package com.trunksbomb.trade.mod.network;
import com.trunksbomb.trade.mod.client.TradeScreen;
import com.trunksbomb.trade.mod.trade.TradeManager;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.neoforge.network.handling.DirectionalPayloadHandler;
@@ -25,6 +23,10 @@ public final class TradeNetworking {
TradeActionPayload.TYPE,
TradeActionPayload.STREAM_CODEC,
TradeNetworking::handleTradeActionServer);
registrar.playToServer(
TradeRequestPayload.TYPE,
TradeRequestPayload.STREAM_CODEC,
TradeNetworking::handleTradeRequestServer);
registrar.playToServer(
DebugTradeControlPayload.TYPE,
DebugTradeControlPayload.STREAM_CODEC,
@@ -32,31 +34,11 @@ public final class TradeNetworking {
}
private static void handleTradeStateClient(TradeStatePayload payload, IPayloadContext context) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null) {
return;
}
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(payload.view().sessionId())) {
screen.updateView(payload.view());
return;
}
minecraft.setScreen(new TradeScreen(
new TradeScreen.TradeMenu(0, minecraft.player.getInventory(), payload.view()),
minecraft.player.getInventory(),
Component.literal("Trade")));
invokeClientState("showOrUpdate", new Class<?>[] {payload.view().getClass()}, payload.view());
}
private static void handleTradeCloseClient(TradeClosePayload payload, IPayloadContext context) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(payload.sessionId())) {
minecraft.setScreen(null);
}
if (minecraft.player != null) {
minecraft.player.displayClientMessage(payload.reason(), false);
}
invokeClientState("closeTrade", new Class<?>[] {java.util.UUID.class, Component.class}, payload.sessionId(), payload.reason());
}
private static void handleTradeActionServer(TradeActionPayload payload, IPayloadContext context) {
@@ -65,6 +47,19 @@ public final class TradeNetworking {
}
}
private static void handleTradeRequestServer(TradeRequestPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) {
return;
}
if (!(player.serverLevel().getEntity(payload.targetEntityId()) instanceof ServerPlayer target)) {
player.sendSystemMessage(Component.literal("You must be looking at a player to trade."));
return;
}
TradeManager.get(player.server).requestTrade(player, target);
}
private static void handleDebugTradeControlServer(DebugTradeControlPayload payload, IPayloadContext context) {
if (context.player() instanceof ServerPlayer player) {
TradeManager.get(player.server).handleDebugControl(player, payload.sessionId(), payload.action(), payload.spec());
@@ -74,4 +69,13 @@ public final class TradeNetworking {
private static void ignoreTradeStateServer(TradeStatePayload payload, IPayloadContext context) {}
private static void ignoreTradeCloseServer(TradeClosePayload payload, IPayloadContext context) {}
private static void invokeClientState(String methodName, Class<?>[] parameterTypes, Object... arguments) {
try {
Class<?> clientState = Class.forName("com.trunksbomb.trade.mod.client.TradeClientState");
clientState.getMethod(methodName, parameterTypes).invoke(null, arguments);
} catch (ReflectiveOperationException exception) {
throw new RuntimeException("Failed to invoke TradeClientState#" + methodName, exception);
}
}
}

View File

@@ -0,0 +1,27 @@
package com.trunksbomb.trade.mod.network;
import com.trunksbomb.trade.mod.TradeMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record TradeRequestPayload(int targetEntityId) implements CustomPacketPayload {
public static final Type<TradeRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TradeMod.MODID, "trade_request"));
public static final StreamCodec<RegistryFriendlyByteBuf, TradeRequestPayload> STREAM_CODEC = new StreamCodec<>() {
@Override
public void encode(RegistryFriendlyByteBuf buf, TradeRequestPayload value) {
buf.writeVarInt(value.targetEntityId());
}
@Override
public TradeRequestPayload decode(RegistryFriendlyByteBuf buf) {
return new TradeRequestPayload(buf.readVarInt());
}
};
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -1,11 +1,14 @@
package com.trunksbomb.trade.mod.trade;
import com.trunksbomb.trade.mod.TradeConfig;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Style;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
@@ -17,6 +20,7 @@ public class TradeManager {
private final Map<UUID, UUID> sessionByPlayer = new HashMap<>();
private final Map<UUID, DebugTradeSession> debugSessionsById = new HashMap<>();
private final Map<UUID, UUID> debugSessionByPlayer = new HashMap<>();
private final Map<UUID, TradeRequest> pendingRequestsByTarget = new HashMap<>();
public static TradeManager get(MinecraftServer server) {
return INSTANCES.computeIfAbsent(server, ignored -> new TradeManager());
@@ -37,6 +41,82 @@ public class TradeManager {
return true;
}
public boolean requestTrade(ServerPlayer requester, ServerPlayer target) {
if (requester == target) {
requester.sendSystemMessage(Component.literal("You cannot trade with yourself."));
return false;
}
if (isTrading(requester) || isTrading(target)) {
requester.sendSystemMessage(Component.literal("Trade could not be started. One of you is already trading."));
return false;
}
if (!withinTradeRange(requester, target)) {
requester.sendSystemMessage(Component.literal("That player is too far away to trade."));
return false;
}
TradeRequest existing = pendingRequestsByTarget.get(target.getUUID());
if (existing != null && existing.requester().equals(requester.getUUID())) {
requester.sendSystemMessage(Component.literal("Trade request already sent to " + target.getGameProfile().getName() + "."));
return false;
}
pendingRequestsByTarget.put(target.getUUID(), new TradeRequest(requester.getUUID(), target.getUUID()));
requester.sendSystemMessage(Component.literal("Trade request sent to " + target.getGameProfile().getName() + "."));
target.sendSystemMessage(Component.literal(requester.getGameProfile().getName() + " would like to trade with you: ")
.append(Component.literal("click to accept")
.withStyle(Style.EMPTY
.withColor(0x55FF55)
.withUnderlined(true)
.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/trade yes")))));
target.sendSystemMessage(Component.literal("You can also use /trade yes or /trade no."));
return true;
}
public boolean acceptPendingTrade(ServerPlayer target) {
TradeRequest request = pendingRequestsByTarget.remove(target.getUUID());
if (request == null) {
target.sendSystemMessage(Component.literal("You have no pending trade request."));
return false;
}
ServerPlayer requester = target.server.getPlayerList().getPlayer(request.requester());
if (requester == null) {
target.sendSystemMessage(Component.literal("That trade request is no longer valid."));
return false;
}
if (!withinTradeRange(requester, target)) {
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " is too far away to trade."));
target.sendSystemMessage(Component.literal(requester.getGameProfile().getName() + " is too far away to trade."));
return false;
}
if (!startTrade(requester, target)) {
target.sendSystemMessage(Component.literal("Trade could not be started. One of you is already trading."));
return false;
}
return true;
}
public boolean declinePendingTrade(ServerPlayer target) {
TradeRequest request = pendingRequestsByTarget.remove(target.getUUID());
if (request == null) {
target.sendSystemMessage(Component.literal("You have no pending trade request."));
return false;
}
ServerPlayer requester = target.server.getPlayerList().getPlayer(request.requester());
if (requester != null) {
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " declined your trade request."));
}
target.sendSystemMessage(Component.literal("Trade request declined."));
return true;
}
public void handleAction(ServerPlayer player, UUID sessionId, TradeAction action, int slot, int amount) {
TradeSession session = sessionsById.get(sessionId);
if (session != null && session.involves(player)) {
@@ -124,6 +204,8 @@ public class TradeManager {
}
public void handleDisconnect(ServerPlayer player) {
clearPendingRequests(player);
TradeSession session = getSession(player);
if (session != null) {
cancel(session, Component.literal(player.getGameProfile().getName() + " left the game."));
@@ -306,4 +388,39 @@ public class TradeManager {
debugSessionsById.remove(session.id());
debugSessionByPlayer.remove(session.player().getUUID());
}
private boolean withinTradeRange(ServerPlayer first, ServerPlayer second) {
int maxDistance = TradeConfig.tradeCommandProximity();
if (maxDistance <= 0) {
return true;
}
if (first.level() != second.level()) {
return false;
}
double maxDistanceSquared = maxDistance * maxDistance;
return first.distanceToSqr(second) <= maxDistanceSquared;
}
private void clearPendingRequests(ServerPlayer player) {
TradeRequest direct = pendingRequestsByTarget.remove(player.getUUID());
if (direct != null) {
ServerPlayer requester = player.server.getPlayerList().getPlayer(direct.requester());
if (requester != null) {
requester.sendSystemMessage(Component.literal("Your trade request to " + player.getGameProfile().getName() + " expired."));
}
}
pendingRequestsByTarget.entrySet().removeIf(entry -> {
if (!entry.getValue().requester().equals(player.getUUID())) {
return false;
}
ServerPlayer target = player.server.getPlayerList().getPlayer(entry.getValue().target());
if (target != null) {
target.sendSystemMessage(Component.literal("The trade request from " + player.getGameProfile().getName() + " expired."));
}
return true;
});
}
private record TradeRequest(UUID requester, UUID target) {}
}

View File

@@ -1,4 +1,5 @@
{
"key.trade.request_trade": "Request Trade",
"trade.trade.title": "Trading Screen",
"trade.trade.offer": "Offer screen",
"trade.trade.confirm": "Confirmation screen"