hide trade debug commands by default, config option

add trade chat commands to ignore/unignore a player, initiate trade and accept/decline trade.
Add safety checks so a player doesn't get into a precarious situation and then get into a trade they initiated earlier.
Trade cancels if one or both players become unsafe
This commit is contained in:
trunksbomb
2026-03-25 01:10:48 -04:00
parent b32a13ab84
commit a5bc9789a3
10 changed files with 1154 additions and 20 deletions

View File

@@ -51,9 +51,5 @@ public final class TradeClientState {
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(sessionId)) {
minecraft.setScreen(null);
}
if (minecraft.player != null) {
minecraft.player.displayClientMessage(reason, false);
}
}
}

View File

@@ -3,6 +3,7 @@ package com.trunksbomb.trade.mod.client;
import com.trunksbomb.trade.mod.network.TradeActionPayload;
import com.trunksbomb.trade.mod.network.DebugTradeControlPayload;
import com.trunksbomb.trade.mod.trade.DebugControlAction;
import com.trunksbomb.trade.mod.trade.DebugUnsafeState;
import com.trunksbomb.trade.mod.trade.TradeAction;
import com.trunksbomb.trade.mod.trade.TradeStage;
import com.trunksbomb.trade.mod.trade.TradeView;
@@ -45,6 +46,7 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
private static final int DEBUG_BUTTON_WIDTH = 86;
private static final int DEBUG_BUTTON_HEIGHT = 20;
private static final int DEBUG_BUTTON_GAP = 4;
private static final int DEBUG_SMALL_BUTTON_WIDTH = 41;
private ContextMenu contextMenu;
public TradeScreen(TradeMenu menu, Inventory inventory, Component title) {
@@ -91,6 +93,18 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
addRenderableWidget(Button.builder(Component.literal("Remove Item"), button -> sendDebug(DebugControlAction.REMOVE_LAST))
.bounds(debugX, debugY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DEBUG_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
.build());
int unsafeY = debugY + 4 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP) + 6;
addRenderableWidget(debugUnsafeButton("Damage", debugX, unsafeY, DebugUnsafeState.DAMAGE));
addRenderableWidget(debugUnsafeButton("Fire", debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY, DebugUnsafeState.FIRE));
addRenderableWidget(debugUnsafeButton("Liquid", debugX, unsafeY + DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP, DebugUnsafeState.LIQUID));
addRenderableWidget(debugUnsafeButton("Moving", debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY + DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP, DebugUnsafeState.MOVING));
addRenderableWidget(debugUnsafeButton("Sleep", debugX, unsafeY + 2 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DebugUnsafeState.SLEEPING));
addRenderableWidget(debugUnsafeButton("Ride", debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY + 2 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DebugUnsafeState.RIDING));
addRenderableWidget(debugUnsafeButton("Glide", debugX, unsafeY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DebugUnsafeState.GLIDING));
addRenderableWidget(Button.builder(Component.literal("Clear"), button -> sendDebug(DebugControlAction.CLEAR_UNSAFE))
.bounds(debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DEBUG_SMALL_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
.build());
}
}
@@ -194,10 +208,20 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
}
private void sendDebug(DebugControlAction action) {
sendDebug(action, "");
}
private void sendDebug(DebugControlAction action, String spec) {
if (minecraft == null || minecraft.getConnection() == null) {
return;
}
PacketDistributor.sendToServer(new DebugTradeControlPayload(menu.view().sessionId(), action, ""));
PacketDistributor.sendToServer(new DebugTradeControlPayload(menu.view().sessionId(), action, spec));
}
private Button debugUnsafeButton(String label, int x, int y, DebugUnsafeState state) {
return Button.builder(Component.literal(label), button -> sendDebug(DebugControlAction.SET_UNSAFE, state.name()))
.bounds(x, y, DEBUG_SMALL_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
.build();
}
private void drawCenteredColumnText(GuiGraphics guiGraphics, String text, int y, int color) {

View File

@@ -1,8 +1,11 @@
package com.trunksbomb.trade.mod.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.trunksbomb.trade.mod.TradeConfig;
import com.trunksbomb.trade.mod.trade.DebugUnsafeState;
import com.trunksbomb.trade.mod.trade.DebugTradeSession;
import com.trunksbomb.trade.mod.trade.TradeManager;
import java.util.concurrent.CompletableFuture;
@@ -15,9 +18,17 @@ public final class TradeCommand {
private TradeCommand() {}
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal("trade")
var trade = Commands.literal("trade")
.then(Commands.literal("debug")
.then(Commands.literal("init").executes(context -> initDebug(context.getSource())))
.requires(source -> TradeConfig.enableDebugFeaturesSafe())
.then(Commands.literal("init")
.executes(context -> initDebug(context.getSource(), 0))
.then(Commands.argument("time", IntegerArgumentType.integer(0))
.executes(context -> initDebug(context.getSource(), IntegerArgumentType.getInteger(context, "time")))))
.then(Commands.literal("request")
.executes(context -> requestDebug(context.getSource(), 0))
.then(Commands.argument("time", IntegerArgumentType.integer(0))
.executes(context -> requestDebug(context.getSource(), IntegerArgumentType.getInteger(context, "time")))))
.then(Commands.literal("offer")
.then(Commands.argument("spec", StringArgumentType.greedyString())
.executes(context -> setDebugOffer(
@@ -28,11 +39,34 @@ public final class TradeCommand {
.executes(context -> removeDebugOffer(
context.getSource(),
StringArgumentType.getString(context, "spec")))))
.then(Commands.literal("unsafe")
.then(Commands.literal("damage").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.DAMAGE, "damage")))
.then(Commands.literal("fire").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.FIRE, "fire")))
.then(Commands.literal("liquid").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.LIQUID, "liquid")))
.then(Commands.literal("moving").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.MOVING, "moving")))
.then(Commands.literal("sleeping").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.SLEEPING, "sleeping")))
.then(Commands.literal("riding").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.RIDING, "riding")))
.then(Commands.literal("gliding").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.GLIDING, "gliding")))
.then(Commands.literal("clear").executes(context -> clearDebugUnsafe(context.getSource()))))
.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("close").executes(context -> closeDebug(context.getSource()))));
dispatcher.register(trade
.then(Commands.literal("yes").executes(context -> respondTrade(context.getSource(), true)))
.then(Commands.literal("no").executes(context -> respondTrade(context.getSource(), false)))
.then(Commands.literal("toggle").executes(context -> toggleTrade(context.getSource())))
.then(Commands.literal("on").executes(context -> setTradeEnabled(context.getSource(), true)))
.then(Commands.literal("off").executes(context -> setTradeEnabled(context.getSource(), false)))
.then(Commands.literal("ignore")
.then(Commands.argument("player", StringArgumentType.word())
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
.executes(context -> ignorePlayer(context.getSource(), StringArgumentType.getString(context, "player")))))
.then(Commands.literal("unignore")
.then(Commands.argument("player", StringArgumentType.word())
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
.executes(context -> unignorePlayer(context.getSource(), StringArgumentType.getString(context, "player")))))
.then(Commands.literal("ignorelist").executes(context -> listIgnoredPlayers(context.getSource())))
.then(Commands.argument("player", StringArgumentType.word())
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
.executes(context -> requestTrade(context.getSource(), StringArgumentType.getString(context, "player")))));
@@ -78,23 +112,143 @@ public final class TradeCommand {
}
boolean result = accept
? TradeManager.get(source.getServer()).acceptPendingTrade(player)
? acceptPendingTrade(player, source)
: TradeManager.get(source.getServer()).declinePendingTrade(player);
return result ? 1 : 0;
}
private static int initDebug(CommandSourceStack source) {
private static boolean acceptPendingTrade(ServerPlayer player, CommandSourceStack source) {
TradeManager manager = TradeManager.get(source.getServer());
if (manager.hasPendingTradeRequest(player)) {
return manager.acceptPendingTrade(player);
}
if (manager.hasPendingDebugTradeRequest(player)) {
return manager.acceptPendingDebugTrade(player);
}
player.sendSystemMessage(Component.literal("You have no pending trade request."));
return false;
}
private static int toggleTrade(CommandSourceStack source) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can toggle trade requests."));
return 0;
}
boolean enabled = TradeManager.get(source.getServer()).toggleTradeEnabled(player);
source.sendSuccess(() -> Component.literal("Trade requests are now " + (enabled ? "enabled" : "disabled") + "."), false);
return 1;
}
private static int setTradeEnabled(CommandSourceStack source, boolean enabled) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can change trade request settings."));
return 0;
}
TradeManager.get(source.getServer()).setTradeEnabled(player, enabled);
source.sendSuccess(() -> Component.literal("Trade requests are now " + (enabled ? "enabled" : "disabled") + "."), false);
return 1;
}
private static int ignorePlayer(CommandSourceStack source, String targetName) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can manage ignored traders."));
return 0;
}
ServerPlayer target = source.getServer().getPlayerList().getPlayerByName(targetName);
if (target == null) {
source.sendFailure(Component.literal("That player is not online."));
return 0;
}
if (target == player) {
source.sendFailure(Component.literal("You cannot ignore yourself."));
return 0;
}
if (!TradeManager.get(source.getServer()).addIgnoredPlayer(player, target)) {
source.sendFailure(Component.literal(target.getGameProfile().getName() + " is already ignored."));
return 0;
}
source.sendSuccess(() -> Component.literal("You are now ignoring trade requests from " + target.getGameProfile().getName() + "."), false);
return 1;
}
private static int unignorePlayer(CommandSourceStack source, String targetName) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can manage ignored traders."));
return 0;
}
ServerPlayer target = source.getServer().getPlayerList().getPlayerByName(targetName);
if (target == null) {
source.sendFailure(Component.literal("That player is not online."));
return 0;
}
if (!TradeManager.get(source.getServer()).removeIgnoredPlayer(player, target)) {
source.sendFailure(Component.literal(target.getGameProfile().getName() + " is not on your ignore list."));
return 0;
}
source.sendSuccess(() -> Component.literal("You are no longer ignoring trade requests from " + target.getGameProfile().getName() + "."), false);
return 1;
}
private static int listIgnoredPlayers(CommandSourceStack source) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can manage ignored traders."));
return 0;
}
java.util.List<String> ignored = TradeManager.get(source.getServer()).ignoredPlayerNames(player);
if (ignored.isEmpty()) {
source.sendSuccess(() -> Component.literal("Your trade ignore list is empty."), false);
return 1;
}
source.sendSuccess(() -> Component.literal("Ignored traders: " + String.join(", ", ignored)), false);
return 1;
}
private static int initDebug(CommandSourceStack source, int delaySeconds) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can start debug trades."));
return 0;
}
if (!TradeManager.get(source.getServer()).initDebugTrade(player)) {
if (!TradeManager.get(source.getServer()).startDebugInit(player, delaySeconds)) {
source.sendFailure(Component.literal("You are already in a trade."));
return 0;
}
source.sendSuccess(() -> Component.literal("Debug trade initialized."), false);
if (delaySeconds <= 0) {
source.sendSuccess(() -> Component.literal("Debug trade request sent and the debug trader will accept immediately."), false);
} else {
source.sendSuccess(() -> Component.literal("Debug trade request sent and the debug trader will accept in " + delaySeconds + " seconds."), false);
}
return 1;
}
private static int requestDebug(CommandSourceStack source, int delaySeconds) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can start debug trades."));
return 0;
}
if (!TradeManager.get(source.getServer()).scheduleDebugRequest(player, delaySeconds, false)) {
source.sendFailure(Component.literal("You are already in a trade."));
return 0;
}
if (delaySeconds <= 0) {
source.sendSuccess(() -> Component.literal("Debug trade request sent."), false);
} else {
source.sendSuccess(() -> Component.literal("Debug trade request scheduled in " + delaySeconds + " seconds."), false);
}
return 1;
}
@@ -155,6 +309,36 @@ public final class TradeCommand {
return runDebugAction(source, TradeManager.get(source.getServer())::closeDebug, "Debug trade closed.");
}
private static int setDebugUnsafe(CommandSourceStack source, DebugUnsafeState state, String label) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can control debug trades."));
return 0;
}
if (!TradeManager.get(source.getServer()).setDebugUnsafeState(player, state)) {
source.sendFailure(Component.literal("Start a debug trade first with /trade debug init."));
return 0;
}
source.sendSuccess(() -> Component.literal("Debug unsafe state enabled: " + label + "."), false);
return 1;
}
private static int clearDebugUnsafe(CommandSourceStack source) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can control debug trades."));
return 0;
}
if (!TradeManager.get(source.getServer()).clearDebugUnsafeStates(player)) {
source.sendFailure(Component.literal("Start a debug trade first with /trade debug init."));
return 0;
}
source.sendSuccess(() -> Component.literal("Debug unsafe states cleared."), false);
return 1;
}
private static int runDebugAction(CommandSourceStack source, java.util.function.Predicate<ServerPlayer> action, String successMessage) {
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Only players can control debug trades."));

View File

@@ -5,6 +5,19 @@ import net.neoforged.neoforge.common.ModConfigSpec;
public final class TradeConfig {
public static final ModConfigSpec SPEC;
private static final ModConfigSpec.IntValue TRADE_COMMAND_PROXIMITY;
private static final ModConfigSpec.IntValue REQUEST_TIMEOUT_SECONDS;
private static final ModConfigSpec.BooleanValue ENABLE_DEBUG_FEATURES;
private static final ModConfigSpec.BooleanValue REQUIRE_ON_GROUND;
private static final ModConfigSpec.BooleanValue REQUIRE_STATIONARY;
private static final ModConfigSpec.DoubleValue STATIONARY_SPEED_THRESHOLD;
private static final ModConfigSpec.BooleanValue REQUIRE_NO_RECENT_DAMAGE;
private static final ModConfigSpec.IntValue NO_DAMAGE_SECONDS;
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_ON_FIRE;
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_IN_LIQUID;
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_SLEEPING;
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_FALL_FLYING;
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_RIDING;
private static final ModConfigSpec.BooleanValue REQUIRE_SAME_DIMENSION;
static {
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
@@ -12,6 +25,33 @@ public final class TradeConfig {
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);
REQUEST_TIMEOUT_SECONDS = builder
.comment("Seconds before a trade request expires.")
.defineInRange("requestTimeoutSeconds", 30, 1, 3600);
ENABLE_DEBUG_FEATURES = builder.comment("Enable debug trade commands and debug UI/testing tools.")
.define("enableDebugFeatures", false);
REQUIRE_ON_GROUND = builder.comment("Require players to be on solid ground before requesting or accepting a trade.")
.define("requireOnGround", true);
REQUIRE_STATIONARY = builder.comment("Require players to be stationary before requesting or accepting a trade.")
.define("requireStationary", true);
STATIONARY_SPEED_THRESHOLD = builder.comment("Maximum horizontal player speed considered stationary.")
.defineInRange("stationarySpeedThreshold", 0.03D, 0.0D, 10.0D);
REQUIRE_NO_RECENT_DAMAGE = builder.comment("Require players to have taken no damage recently before requesting or accepting a trade.")
.define("requireNoRecentDamage", true);
NO_DAMAGE_SECONDS = builder.comment("How long a player must be out of combat to trade when requireNoRecentDamage is enabled.")
.defineInRange("noDamageSeconds", 10, 0, 3600);
REQUIRE_NOT_ON_FIRE = builder.comment("Require players to not be on fire before requesting or accepting a trade.")
.define("requireNotOnFire", true);
REQUIRE_NOT_IN_LIQUID = builder.comment("Require players to not be in water or lava before requesting or accepting a trade.")
.define("requireNotInLiquid", true);
REQUIRE_NOT_SLEEPING = builder.comment("Require players to not be sleeping before requesting or accepting a trade.")
.define("requireNotSleeping", true);
REQUIRE_NOT_FALL_FLYING = builder.comment("Require players to not be gliding with elytra before requesting or accepting a trade.")
.define("requireNotFallFlying", true);
REQUIRE_NOT_RIDING = builder.comment("Require players to not be riding another entity before requesting or accepting a trade.")
.define("requireNotRiding", true);
REQUIRE_SAME_DIMENSION = builder.comment("Require both players to be in the same dimension.")
.define("requireSameDimension", true);
builder.pop();
SPEC = builder.build();
}
@@ -21,4 +61,64 @@ public final class TradeConfig {
public static int tradeCommandProximity() {
return TRADE_COMMAND_PROXIMITY.get();
}
public static int requestTimeoutSeconds() {
return REQUEST_TIMEOUT_SECONDS.get();
}
public static boolean enableDebugFeatures() {
return ENABLE_DEBUG_FEATURES.get();
}
public static boolean enableDebugFeaturesSafe() {
try {
return ENABLE_DEBUG_FEATURES.get();
} catch (IllegalStateException ignored) {
return false;
}
}
public static boolean requireOnGround() {
return REQUIRE_ON_GROUND.get();
}
public static boolean requireStationary() {
return REQUIRE_STATIONARY.get();
}
public static double stationarySpeedThreshold() {
return STATIONARY_SPEED_THRESHOLD.get();
}
public static boolean requireNoRecentDamage() {
return REQUIRE_NO_RECENT_DAMAGE.get();
}
public static int noDamageSeconds() {
return NO_DAMAGE_SECONDS.get();
}
public static boolean requireNotOnFire() {
return REQUIRE_NOT_ON_FIRE.get();
}
public static boolean requireNotInLiquid() {
return REQUIRE_NOT_IN_LIQUID.get();
}
public static boolean requireNotSleeping() {
return REQUIRE_NOT_SLEEPING.get();
}
public static boolean requireNotFallFlying() {
return REQUIRE_NOT_FALL_FLYING.get();
}
public static boolean requireNotRiding() {
return REQUIRE_NOT_RIDING.get();
}
public static boolean requireSameDimension() {
return REQUIRE_SAME_DIMENSION.get();
}
}

View File

@@ -26,6 +26,8 @@ public class TradeMod {
NeoForge.EVENT_BUS.addListener(this::registerCommands);
NeoForge.EVENT_BUS.addListener(this::onPlayerLogout);
NeoForge.EVENT_BUS.addListener(this::onItemPickup);
NeoForge.EVENT_BUS.addListener(this::onServerTick);
NeoForge.EVENT_BUS.addListener(this::onLivingDamage);
}
private void commonSetup(FMLCommonSetupEvent event) {
@@ -53,4 +55,14 @@ public class TradeMod {
event.setCanPickup(net.neoforged.neoforge.common.util.TriState.FALSE);
}
}
public void onServerTick(net.neoforged.neoforge.event.tick.ServerTickEvent.Post event) {
TradeManager.get(event.getServer()).tick(event.getServer());
}
public void onLivingDamage(net.neoforged.neoforge.event.entity.living.LivingDamageEvent.Post event) {
if (event.getEntity() instanceof net.minecraft.server.level.ServerPlayer player) {
TradeManager.get(player.server).recordDamage(player);
}
}
}

View File

@@ -5,6 +5,8 @@ public enum DebugControlAction {
APPEND_RANDOM,
REMOVE_OFFER,
REMOVE_LAST,
SET_UNSAFE,
CLEAR_UNSAFE,
ACCEPT,
CANCEL,
CLOSE

View File

@@ -3,6 +3,7 @@ package com.trunksbomb.trade.mod.trade;
import com.trunksbomb.trade.mod.network.TradeClosePayload;
import com.trunksbomb.trade.mod.network.TradeStatePayload;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
import net.minecraft.core.registries.BuiltInRegistries;
@@ -25,6 +26,7 @@ public class DebugTradeSession {
private final List<ItemStack> inventorySnapshot;
private final List<TradeEntry> selfOffer = blankOffer();
private final List<ItemStack> otherOffer = blankStacks();
private final EnumSet<DebugUnsafeState> unsafeStates = EnumSet.noneOf(DebugUnsafeState.class);
private boolean selfAccepted;
private boolean otherAccepted;
private TradeStage stage = TradeStage.OFFERING;
@@ -192,6 +194,22 @@ public class DebugTradeSession {
return false;
}
public void setUnsafeState(DebugUnsafeState state, boolean unsafe) {
if (unsafe) {
unsafeStates.add(state);
} else {
unsafeStates.remove(state);
}
}
public void clearUnsafeStates() {
unsafeStates.clear();
}
public boolean hasUnsafeState(DebugUnsafeState state) {
return unsafeStates.contains(state);
}
public void advanceToConfirmation() {
if (stage == TradeStage.OFFERING && selfAccepted && otherAccepted) {
stage = TradeStage.CONFIRMING;

View File

@@ -0,0 +1,11 @@
package com.trunksbomb.trade.mod.trade;
public enum DebugUnsafeState {
DAMAGE,
FIRE,
LIQUID,
MOVING,
SLEEPING,
RIDING,
GLIDING
}

View File

@@ -1,6 +1,7 @@
package com.trunksbomb.trade.mod.trade;
import com.trunksbomb.trade.mod.TradeConfig;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -8,12 +9,17 @@ 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.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
public class TradeManager {
private static final long ACCEPT_GRACE_TICKS = 5L * 20L;
private static final long ACCEPT_HOLD_TICKS = 20L;
private static final Map<MinecraftServer, TradeManager> INSTANCES = new WeakHashMap<>();
private final Map<UUID, TradeSession> sessionsById = new HashMap<>();
@@ -21,13 +27,18 @@ public class TradeManager {
private final Map<UUID, DebugTradeSession> debugSessionsById = new HashMap<>();
private final Map<UUID, UUID> debugSessionByPlayer = new HashMap<>();
private final Map<UUID, TradeRequest> pendingRequestsByTarget = new HashMap<>();
private final Map<UUID, ScheduledDebugRequest> scheduledDebugRequestsByTarget = new HashMap<>();
private final Map<UUID, DebugTradeRequest> pendingDebugRequestsByTarget = new HashMap<>();
private final Map<UUID, PendingAcceptance> pendingAcceptancesByRequester = new HashMap<>();
private final Map<UUID, UUID> pendingAcceptanceByPlayer = new HashMap<>();
private final Map<UUID, Long> lastDamageTickByPlayer = new HashMap<>();
public static TradeManager get(MinecraftServer server) {
return INSTANCES.computeIfAbsent(server, ignored -> new TradeManager());
}
public boolean startTrade(ServerPlayer first, ServerPlayer second) {
if (isTrading(first) || isTrading(second)) {
if (isBusy(first) || isBusy(second)) {
return false;
}
@@ -47,23 +58,45 @@ public class TradeManager {
return false;
}
if (isTrading(requester) || isTrading(target)) {
if (isBusy(requester) || isBusy(target)) {
requester.sendSystemMessage(Component.literal("Trade could not be started. One of you is already trading."));
return false;
}
if (!preferences(requester.server).isTradeEnabled(target.getUUID())) {
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " is not accepting trade requests."));
return false;
}
if (preferences(requester.server).isIgnoring(target.getUUID(), requester.getUUID())) {
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " is ignoring your trade requests."));
return false;
}
if (!withinTradeRange(requester, target)) {
requester.sendSystemMessage(Component.literal("That player is too far away to trade."));
return false;
}
List<Component> requesterUnsafe = tradeSafetyFailures(requester, target.server.getTickCount());
if (!requesterUnsafe.isEmpty()) {
requester.sendSystemMessage(Component.literal("You cannot request a trade right now: ").append(joinReasons(requesterUnsafe)));
return false;
}
List<Component> targetUnsafe = tradeSafetyFailures(target, target.server.getTickCount());
if (!targetUnsafe.isEmpty()) {
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " is not in a safe state to trade right now: ").append(joinReasons(targetUnsafe)));
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()));
pendingRequestsByTarget.put(target.getUUID(), new TradeRequest(requester.getUUID(), target.getUUID(), target.server.getTickCount()));
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")
@@ -88,12 +121,37 @@ public class TradeManager {
return false;
}
if (!preferences(target.server).isTradeEnabled(target.getUUID())) {
target.sendSystemMessage(Component.literal("You are not accepting trade requests right now."));
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " is not accepting trade requests."));
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;
}
long currentTick = target.server.getTickCount();
List<Component> requesterUnsafe = tradeSafetyFailures(requester, currentTick);
if (!requesterUnsafe.isEmpty()) {
if (canDelayTradeAcceptance(requester, currentTick)) {
beginPendingAcceptance(requester, target, currentTick);
return true;
}
requester.sendSystemMessage(Component.literal("Trade request cancelled: you are no longer in a safe state to trade (").append(joinReasons(requesterUnsafe)).append(Component.literal(").")));
target.sendSystemMessage(Component.literal("Trade request cancelled because " + requester.getGameProfile().getName() + " is no longer in a safe state to trade: ").append(joinReasons(requesterUnsafe)));
return false;
}
List<Component> targetUnsafe = tradeSafetyFailures(target, currentTick);
if (!targetUnsafe.isEmpty()) {
target.sendSystemMessage(Component.literal("You cannot accept the trade right now: ").append(joinReasons(targetUnsafe)));
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " is not in a safe state to trade right now: ").append(joinReasons(targetUnsafe)));
return false;
}
if (!startTrade(requester, target)) {
target.sendSystemMessage(Component.literal("Trade could not be started. One of you is already trading."));
return false;
@@ -102,7 +160,24 @@ public class TradeManager {
return true;
}
public boolean hasPendingTradeRequest(ServerPlayer player) {
return pendingRequestsByTarget.containsKey(player.getUUID());
}
public boolean hasPendingDebugTradeRequest(ServerPlayer player) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
return pendingDebugRequestsByTarget.containsKey(player.getUUID());
}
public boolean declinePendingTrade(ServerPlayer target) {
DebugTradeRequest debugRequest = pendingDebugRequestsByTarget.remove(target.getUUID());
if (debugRequest != null) {
target.sendSystemMessage(Component.literal("Debug trade request declined."));
return true;
}
TradeRequest request = pendingRequestsByTarget.remove(target.getUUID());
if (request == null) {
target.sendSystemMessage(Component.literal("You have no pending trade request."));
@@ -168,6 +243,14 @@ public class TradeManager {
}
private void handleDebugAction(DebugTradeSession session, TradeAction action, int slot, int amount) {
if (action == TradeAction.ACCEPT) {
List<Component> unsafe = tradeSafetyFailures(session, session.player(), session.player().server.getTickCount());
if (!unsafe.isEmpty()) {
session.player().sendSystemMessage(Component.literal("You cannot accept the debug trade right now: ").append(joinReasons(unsafe)));
return;
}
}
boolean changed = switch (action) {
case ADD_ITEM -> session.addFromInventory(slot, amount);
case REMOVE_ITEM -> session.removeFromOffer(slot, amount);
@@ -205,6 +288,8 @@ public class TradeManager {
public void handleDisconnect(ServerPlayer player) {
clearPendingRequests(player);
clearDebugRequests(player);
clearPendingAcceptance(player, null, null);
TradeSession session = getSession(player);
if (session != null) {
@@ -222,8 +307,57 @@ public class TradeManager {
return sessionByPlayer.containsKey(player.getUUID()) || debugSessionByPlayer.containsKey(player.getUUID());
}
public void recordDamage(ServerPlayer player) {
lastDamageTickByPlayer.put(player.getUUID(), (long) player.server.getTickCount());
}
public void tick(MinecraftServer server) {
expireRequests(server);
processScheduledDebugRequests(server);
processPendingAcceptances(server);
validateActiveTrades(server);
}
public boolean setTradeEnabled(ServerPlayer player, boolean enabled) {
TradePreferencesData preferences = preferences(player.server);
boolean changed = preferences.isTradeEnabled(player.getUUID()) != enabled;
preferences.setTradeEnabled(player.getUUID(), enabled);
return changed;
}
public boolean toggleTradeEnabled(ServerPlayer player) {
boolean enabled = !preferences(player.server).isTradeEnabled(player.getUUID());
preferences(player.server).setTradeEnabled(player.getUUID(), enabled);
return enabled;
}
public boolean isTradeEnabled(ServerPlayer player) {
return preferences(player.server).isTradeEnabled(player.getUUID());
}
public boolean addIgnoredPlayer(ServerPlayer player, ServerPlayer ignored) {
return preferences(player.server).addIgnoredPlayer(player.getUUID(), ignored.getUUID());
}
public boolean removeIgnoredPlayer(ServerPlayer player, ServerPlayer ignored) {
return preferences(player.server).removeIgnoredPlayer(player.getUUID(), ignored.getUUID());
}
public List<String> ignoredPlayerNames(ServerPlayer player) {
List<String> result = new ArrayList<>();
for (UUID ignored : preferences(player.server).ignoredPlayers(player.getUUID())) {
ServerPlayer online = player.server.getPlayerList().getPlayer(ignored);
result.add(online != null ? online.getGameProfile().getName() : ignored.toString());
}
result.sort(String::compareToIgnoreCase);
return result;
}
public boolean initDebugTrade(ServerPlayer player) {
if (isTrading(player)) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
if (isBusy(player)) {
return false;
}
@@ -235,7 +369,30 @@ public class TradeManager {
return true;
}
public boolean startDebugInit(ServerPlayer player, int delaySeconds) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
return scheduleDebugRequest(player, delaySeconds, true);
}
public boolean scheduleDebugRequest(ServerPlayer player, int delaySeconds, boolean autoAccept) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
if (isBusy(player) || hasPendingDebugRequest(player)) {
return false;
}
long triggerTick = player.server.getTickCount() + Math.max(0, delaySeconds) * 20L;
scheduledDebugRequestsByTarget.put(player.getUUID(), new ScheduledDebugRequest(player.getUUID(), triggerTick, autoAccept));
return true;
}
public boolean setDebugOffer(ServerPlayer player, List<ItemStack> offer) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return false;
@@ -247,6 +404,9 @@ public class TradeManager {
}
public int removeDebugOffer(ServerPlayer player, String spec) {
if (!TradeConfig.enableDebugFeatures()) {
return -1;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return -1;
@@ -262,11 +422,20 @@ public class TradeManager {
}
public boolean acceptDebug(ServerPlayer player) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return false;
}
List<Component> unsafe = tradeSafetyFailures(session, player, player.server.getTickCount());
if (!unsafe.isEmpty()) {
player.sendSystemMessage(Component.literal("You cannot accept the debug trade right now: ").append(joinReasons(unsafe)));
return false;
}
session.acceptOther();
if (!session.isConfirmationStage()) {
session.advanceToConfirmation();
@@ -286,7 +455,38 @@ public class TradeManager {
return true;
}
public boolean setDebugUnsafeState(ServerPlayer player, DebugUnsafeState state) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return false;
}
session.setUnsafeState(state, true);
session.sync();
return true;
}
public boolean clearDebugUnsafeStates(ServerPlayer player) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return false;
}
session.clearUnsafeStates();
session.sync();
return true;
}
public boolean cancelDebug(ServerPlayer player) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return false;
@@ -297,6 +497,9 @@ public class TradeManager {
}
public boolean closeDebug(ServerPlayer player) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeSession session = getDebugSession(player);
if (session == null) {
return false;
@@ -307,6 +510,9 @@ public class TradeManager {
}
public void handleDebugControl(ServerPlayer player, UUID sessionId, DebugControlAction action, String spec) {
if (!TradeConfig.enableDebugFeatures()) {
return;
}
DebugTradeSession session = debugSessionsById.get(sessionId);
if (session == null || session.player() != player) {
return;
@@ -333,6 +539,15 @@ public class TradeManager {
session.sync();
}
}
case SET_UNSAFE -> {
DebugUnsafeState state = parseDebugUnsafeState(spec);
session.setUnsafeState(state, true);
session.sync();
}
case CLEAR_UNSAFE -> {
session.clearUnsafeStates();
session.sync();
}
case ACCEPT -> acceptDebug(player);
case CANCEL -> cancelDebug(player);
case CLOSE -> closeDebug(player);
@@ -390,13 +605,13 @@ public class TradeManager {
}
private boolean withinTradeRange(ServerPlayer first, ServerPlayer second) {
if (TradeConfig.requireSameDimension() && first.level() != second.level()) {
return false;
}
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;
}
@@ -422,5 +637,463 @@ public class TradeManager {
});
}
private record TradeRequest(UUID requester, UUID target) {}
private void clearDebugRequests(ServerPlayer player) {
scheduledDebugRequestsByTarget.remove(player.getUUID());
pendingDebugRequestsByTarget.remove(player.getUUID());
}
private void expireRequests(MinecraftServer server) {
long timeoutTicks = TradeConfig.requestTimeoutSeconds() * 20L;
if (timeoutTicks <= 0) {
return;
}
pendingRequestsByTarget.entrySet().removeIf(entry -> {
TradeRequest request = entry.getValue();
if (server.getTickCount() - request.createdTick() < timeoutTicks) {
return false;
}
ServerPlayer requester = server.getPlayerList().getPlayer(request.requester());
ServerPlayer target = server.getPlayerList().getPlayer(request.target());
if (requester != null) {
requester.sendSystemMessage(Component.literal("Your trade request expired."));
}
if (target != null) {
target.sendSystemMessage(Component.literal("Trade request expired."));
}
return true;
});
}
private void processScheduledDebugRequests(MinecraftServer server) {
List<ScheduledDebugRequest> scheduledRequests = new ArrayList<>(scheduledDebugRequestsByTarget.values());
for (ScheduledDebugRequest scheduled : scheduledRequests) {
if (server.getTickCount() < scheduled.triggerTick()) {
continue;
}
scheduledDebugRequestsByTarget.remove(scheduled.target());
ServerPlayer target = server.getPlayerList().getPlayer(scheduled.target());
if (target == null || isBusy(target)) {
continue;
}
if (scheduled.autoAccept()) {
startOrDelayDebugTrade(target);
continue;
}
pendingDebugRequestsByTarget.put(target.getUUID(), new DebugTradeRequest(target.getUUID(), server.getTickCount()));
target.sendSystemMessage(Component.literal("Debug Trader 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."));
}
}
private void validateActiveTrades(MinecraftServer server) {
List<TradeSession> sessions = new ArrayList<>(sessionsById.values());
for (TradeSession session : sessions) {
ServerPlayer first = session.firstPlayer();
ServerPlayer second = session.secondPlayer();
if (first.isRemoved() || second.isRemoved()) {
cancel(session, Component.literal("Trade cancelled because one player became unavailable."));
continue;
}
if (!withinTradeRange(first, second)) {
cancel(session, Component.literal("Trade cancelled because players moved too far apart."));
continue;
}
List<Component> firstFailures = tradeSafetyFailures(first, server.getTickCount());
if (!firstFailures.isEmpty()) {
cancel(session, Component.literal("Trade cancelled because " + first.getGameProfile().getName() + " is no longer in a safe state to trade: ").append(joinReasons(firstFailures)));
continue;
}
List<Component> secondFailures = tradeSafetyFailures(second, server.getTickCount());
if (!secondFailures.isEmpty()) {
cancel(session, Component.literal("Trade cancelled because " + second.getGameProfile().getName() + " is no longer in a safe state to trade: ").append(joinReasons(secondFailures)));
}
}
List<DebugTradeSession> debugSessions = new ArrayList<>(debugSessionsById.values());
for (DebugTradeSession session : debugSessions) {
ServerPlayer player = session.player();
if (player.isRemoved()) {
closeDebug(session, Component.literal("Debug trade closed because you became unavailable."));
continue;
}
List<Component> failures = tradeSafetyFailures(session, player, server.getTickCount());
if (!failures.isEmpty()) {
closeDebug(session, Component.literal("Debug trade cancelled because you are no longer in a safe state to trade: ").append(joinReasons(failures)));
}
}
}
private void processPendingAcceptances(MinecraftServer server) {
List<PendingAcceptance> pendingAcceptances = new ArrayList<>(pendingAcceptancesByRequester.values());
for (PendingAcceptance pending : pendingAcceptances) {
ServerPlayer requester = server.getPlayerList().getPlayer(pending.requester());
ServerPlayer target = pending.target() == null ? null : server.getPlayerList().getPlayer(pending.target());
if (requester == null || (!pending.debug() && target == null)) {
clearPendingAcceptanceByRequester(pending.requester());
continue;
}
if (isTrading(requester) || (target != null && isTrading(target))) {
clearPendingAcceptanceByRequester(pending.requester());
continue;
}
if (target != null && !withinTradeRange(requester, target)) {
clearPendingAcceptance(
requester,
Component.literal("Trade request cancelled because " + target.getGameProfile().getName() + " is too far away to trade."),
Component.literal("Trade request cancelled because players moved too far apart."));
continue;
}
long currentTick = server.getTickCount();
if (hasNonTransientSafetyFailure(requester, currentTick)) {
List<Component> failures = tradeSafetyFailures(requester, currentTick);
clearPendingAcceptance(
requester,
Component.literal("Trade request cancelled: you are no longer in a safe state to trade (").append(joinReasons(failures)).append(Component.literal(").")),
Component.literal("Trade request cancelled because " + requester.getGameProfile().getName() + " is no longer in a safe state to trade: ").append(joinReasons(failures)));
continue;
}
boolean needsGround = TradeConfig.requireOnGround() && !requester.onGround();
boolean needsStill = TradeConfig.requireStationary() && !isStationary(requester);
Long stationarySinceTick = pending.stationarySinceTick();
if (needsGround || needsStill) {
pending.stationarySinceTick(null);
if (currentTick > pending.deadlineTick()) {
List<Component> failures = tradeSafetyFailures(requester, currentTick);
clearPendingAcceptance(
requester,
Component.literal("Trade request cancelled because you did not get to safety in time: ").append(joinReasons(failures)),
Component.literal("Trade request cancelled because " + requester.getGameProfile().getName() + " did not get to safety in time."));
} else {
showPendingAcceptanceCountdown(requester, pending, needsGround, needsStill, currentTick);
}
continue;
}
if (stationarySinceTick == null) {
if (currentTick > pending.deadlineTick()) {
clearPendingAcceptance(
requester,
Component.literal("Trade request cancelled because you did not stay still long enough."),
Component.literal("Trade request cancelled because " + requester.getGameProfile().getName() + " did not stay still long enough."));
continue;
}
pending.stationarySinceTick(currentTick);
stationarySinceTick = currentTick;
}
long heldTicks = currentTick - stationarySinceTick;
if (heldTicks >= ACCEPT_HOLD_TICKS) {
clearPendingAcceptanceByRequester(pending.requester());
if (pending.debug()) {
if (!initDebugTrade(requester)) {
requester.sendSystemMessage(Component.literal("Debug trade could not be started."));
}
} else if (!startTrade(requester, target)) {
requester.sendSystemMessage(Component.literal("Trade could not be started. One of you is already trading."));
target.sendSystemMessage(Component.literal("Trade could not be started. One of you is already trading."));
}
continue;
}
long remainingHoldTicks = ACCEPT_HOLD_TICKS - heldTicks;
requester.displayClientMessage(Component.literal("Trade opening: hold still for " + formatSecondsTenths(remainingHoldTicks) + "s"), true);
}
}
private List<Component> tradeSafetyFailures(ServerPlayer player, long currentTick) {
List<Component> failures = new ArrayList<>();
if (!player.isAlive() || player.isDeadOrDying()) {
failures.add(Component.literal("you must be alive"));
}
if (TradeConfig.requireOnGround() && !player.onGround()) {
failures.add(Component.literal("you must be on solid ground"));
}
if (TradeConfig.requireStationary() && !isStationary(player)) {
failures.add(Component.literal("you must be standing still"));
}
if (TradeConfig.requireNoRecentDamage()) {
long lastDamageTick = lastDamageTickByPlayer.getOrDefault(player.getUUID(), Long.MIN_VALUE / 4);
long elapsed = currentTick - lastDamageTick;
if (elapsed < TradeConfig.noDamageSeconds() * 20L) {
failures.add(Component.literal("you were damaged too recently"));
}
}
if (TradeConfig.requireNotOnFire() && player.isOnFire()) {
failures.add(Component.literal("you cannot be on fire"));
}
if (TradeConfig.requireNotInLiquid() && (player.isInWaterOrBubble() || player.isInLava())) {
failures.add(Component.literal("you cannot be in liquid"));
}
if (TradeConfig.requireNotSleeping() && player.isSleeping()) {
failures.add(Component.literal("you cannot be sleeping"));
}
if (TradeConfig.requireNotFallFlying() && player.isFallFlying()) {
failures.add(Component.literal("you cannot be gliding"));
}
if (TradeConfig.requireNotRiding() && player.isPassenger()) {
failures.add(Component.literal("you cannot be mounted"));
}
if (player.isSpectator()) {
failures.add(Component.literal("spectators cannot trade"));
}
return failures;
}
private List<Component> tradeSafetyFailures(DebugTradeSession session, ServerPlayer player, long currentTick) {
List<Component> failures = new ArrayList<>();
if (session.hasUnsafeState(DebugUnsafeState.DAMAGE) && TradeConfig.requireNoRecentDamage()) {
failures.add(Component.literal("you were damaged too recently"));
}
if (session.hasUnsafeState(DebugUnsafeState.FIRE) && TradeConfig.requireNotOnFire()) {
failures.add(Component.literal("you cannot be on fire"));
}
if (session.hasUnsafeState(DebugUnsafeState.LIQUID) && TradeConfig.requireNotInLiquid()) {
failures.add(Component.literal("you cannot be in liquid"));
}
if (session.hasUnsafeState(DebugUnsafeState.MOVING) && TradeConfig.requireStationary()) {
failures.add(Component.literal("you must be standing still"));
}
if (session.hasUnsafeState(DebugUnsafeState.SLEEPING) && TradeConfig.requireNotSleeping()) {
failures.add(Component.literal("you cannot be sleeping"));
}
if (session.hasUnsafeState(DebugUnsafeState.RIDING) && TradeConfig.requireNotRiding()) {
failures.add(Component.literal("you cannot be mounted"));
}
if (session.hasUnsafeState(DebugUnsafeState.GLIDING) && TradeConfig.requireNotFallFlying()) {
failures.add(Component.literal("you cannot be gliding"));
}
failures.addAll(tradeSafetyFailures(player, currentTick));
return failures;
}
private Component joinReasons(List<Component> reasons) {
MutableComponent message = Component.empty();
for (int i = 0; i < reasons.size(); i++) {
if (i > 0) {
message = message.append(Component.literal(", "));
}
message = message.append(reasons.get(i));
}
return message;
}
private boolean isBusy(ServerPlayer player) {
return isTrading(player)
|| pendingAcceptanceByPlayer.containsKey(player.getUUID())
|| scheduledDebugRequestsByTarget.containsKey(player.getUUID())
|| pendingDebugRequestsByTarget.containsKey(player.getUUID());
}
private boolean canDelayTradeAcceptance(ServerPlayer requester, long currentTick) {
if (hasNonTransientSafetyFailure(requester, currentTick)) {
return false;
}
return (TradeConfig.requireOnGround() && !requester.onGround())
|| (TradeConfig.requireStationary() && !isStationary(requester));
}
private boolean hasNonTransientSafetyFailure(ServerPlayer player, long currentTick) {
if (!player.isAlive() || player.isDeadOrDying()) {
return true;
}
if (TradeConfig.requireNoRecentDamage()) {
long lastDamageTick = lastDamageTickByPlayer.getOrDefault(player.getUUID(), Long.MIN_VALUE / 4);
long elapsed = currentTick - lastDamageTick;
if (elapsed < TradeConfig.noDamageSeconds() * 20L) {
return true;
}
}
if (TradeConfig.requireNotOnFire() && player.isOnFire()) {
return true;
}
if (TradeConfig.requireNotInLiquid() && (player.isInWaterOrBubble() || player.isInLava())) {
return true;
}
if (TradeConfig.requireNotSleeping() && player.isSleeping()) {
return true;
}
if (TradeConfig.requireNotFallFlying() && player.isFallFlying()) {
return true;
}
if (TradeConfig.requireNotRiding() && player.isPassenger()) {
return true;
}
return player.isSpectator();
}
private void beginPendingAcceptance(ServerPlayer requester, ServerPlayer target, long currentTick) {
clearPendingAcceptanceByRequester(requester.getUUID());
PendingAcceptance pending = new PendingAcceptance(requester.getUUID(), target == null ? null : target.getUUID(), currentTick + ACCEPT_GRACE_TICKS, target == null);
pendingAcceptancesByRequester.put(requester.getUUID(), pending);
pendingAcceptanceByPlayer.put(requester.getUUID(), requester.getUUID());
if (target != null) {
pendingAcceptanceByPlayer.put(target.getUUID(), requester.getUUID());
requester.sendSystemMessage(Component.literal(target.getGameProfile().getName() + " accepted your trade request. Land and stand still to open the trade."));
target.sendSystemMessage(Component.literal("Waiting for " + requester.getGameProfile().getName() + " to land and stand still before opening the trade."));
} else {
requester.sendSystemMessage(Component.literal("Debug Trader accepted your trade request. Land and stand still to open the trade."));
}
showPendingAcceptanceCountdown(requester, pending, TradeConfig.requireOnGround() && !requester.onGround(), TradeConfig.requireStationary() && !isStationary(requester), currentTick);
}
private void showPendingAcceptanceCountdown(ServerPlayer requester, PendingAcceptance pending, boolean needsGround, boolean needsStill, long currentTick) {
long remainingTicks = Math.max(0L, pending.deadlineTick() - currentTick);
MutableComponent message = Component.literal("Trade opening: ");
if (needsGround && needsStill) {
message.append("land and stop moving");
} else if (needsGround) {
message.append("land on solid ground");
} else {
message.append("stop moving");
}
message.append(" within ").append(Component.literal(String.valueOf((remainingTicks + 19L) / 20L))).append("s");
requester.displayClientMessage(message, true);
}
private void clearPendingAcceptance(ServerPlayer requester, Component requesterMessage, Component targetMessage) {
UUID requesterId = requester.getUUID();
PendingAcceptance pending = pendingAcceptancesByRequester.get(requesterId);
if (pending == null) {
return;
}
ServerPlayer liveRequester = requester.server.getPlayerList().getPlayer(pending.requester());
ServerPlayer target = pending.target() == null ? null : requester.server.getPlayerList().getPlayer(pending.target());
clearPendingAcceptanceByRequester(requesterId);
if (requesterMessage != null && liveRequester != null) {
liveRequester.sendSystemMessage(requesterMessage);
}
if (targetMessage != null && target != null) {
target.sendSystemMessage(targetMessage);
}
}
private void clearPendingAcceptanceByRequester(UUID requesterId) {
PendingAcceptance pending = pendingAcceptancesByRequester.remove(requesterId);
if (pending == null) {
return;
}
pendingAcceptanceByPlayer.remove(pending.requester());
if (pending.target() != null) {
pendingAcceptanceByPlayer.remove(pending.target());
}
}
private boolean hasPendingDebugRequest(ServerPlayer player) {
return pendingDebugRequestsByTarget.containsKey(player.getUUID()) || scheduledDebugRequestsByTarget.containsKey(player.getUUID());
}
private boolean startOrDelayDebugTrade(ServerPlayer player) {
long currentTick = player.server.getTickCount();
List<Component> unsafe = tradeSafetyFailures(player, currentTick);
if (!unsafe.isEmpty()) {
if (canDelayTradeAcceptance(player, currentTick)) {
beginPendingAcceptance(player, null, currentTick);
return true;
}
player.sendSystemMessage(Component.literal("Debug trade request cancelled: you are not in a safe state to trade (").append(joinReasons(unsafe)).append(Component.literal(").")));
return false;
}
return initDebugTrade(player);
}
public boolean acceptPendingDebugTrade(ServerPlayer player) {
if (!TradeConfig.enableDebugFeatures()) {
return false;
}
DebugTradeRequest request = pendingDebugRequestsByTarget.remove(player.getUUID());
if (request == null) {
return false;
}
return startOrDelayDebugTrade(player);
}
private String formatSecondsTenths(long ticks) {
long tenths = Math.max(0L, (ticks * 10L + 19L) / 20L);
return String.format(java.util.Locale.ROOT, "%.1f", tenths / 10.0D);
}
private DebugUnsafeState parseDebugUnsafeState(String spec) {
try {
return DebugUnsafeState.valueOf(spec.trim().toUpperCase(java.util.Locale.ROOT));
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("Unknown debug unsafe state: " + spec);
}
}
private boolean isStationary(ServerPlayer player) {
Vec3 movement = player.getDeltaMovement();
double horizontalLengthSqr = (movement.x * movement.x) + (movement.z * movement.z);
double threshold = TradeConfig.stationarySpeedThreshold();
return horizontalLengthSqr <= threshold * threshold;
}
private TradePreferencesData preferences(MinecraftServer server) {
return TradePreferencesData.get(server);
}
private record TradeRequest(UUID requester, UUID target, long createdTick) {}
private record DebugTradeRequest(UUID target, long createdTick) {}
private record ScheduledDebugRequest(UUID target, long triggerTick, boolean autoAccept) {}
private static final class PendingAcceptance {
private final UUID requester;
private final UUID target;
private final long deadlineTick;
private final boolean debug;
private Long stationarySinceTick;
private PendingAcceptance(UUID requester, UUID target, long deadlineTick, boolean debug) {
this.requester = requester;
this.target = target;
this.deadlineTick = deadlineTick;
this.debug = debug;
}
public UUID requester() {
return requester;
}
public UUID target() {
return target;
}
public long deadlineTick() {
return deadlineTick;
}
public boolean debug() {
return debug;
}
public Long stationarySinceTick() {
return stationarySinceTick;
}
public void stationarySinceTick(Long stationarySinceTick) {
this.stationarySinceTick = stationarySinceTick;
}
}
}

View File

@@ -0,0 +1,114 @@
package com.trunksbomb.trade.mod.trade;
import com.mojang.datafixers.util.Pair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.saveddata.SavedData;
public class TradePreferencesData extends SavedData {
private static final String DATA_NAME = "trade_preferences";
private final Map<UUID, Boolean> tradeEnabled = new HashMap<>();
private final Map<UUID, Set<UUID>> ignoredPlayers = new HashMap<>();
public static TradePreferencesData get(MinecraftServer server) {
return server.overworld().getDataStorage().computeIfAbsent(factory(), DATA_NAME);
}
private static Factory<TradePreferencesData> factory() {
return new Factory<>(TradePreferencesData::new, TradePreferencesData::load);
}
private static TradePreferencesData load(CompoundTag tag, HolderLookup.Provider registries) {
TradePreferencesData data = new TradePreferencesData();
CompoundTag enabledTag = tag.getCompound("trade_enabled");
for (String key : enabledTag.getAllKeys()) {
data.tradeEnabled.put(UUID.fromString(key), enabledTag.getBoolean(key));
}
CompoundTag ignoredTag = tag.getCompound("ignored_players");
for (String key : ignoredTag.getAllKeys()) {
ListTag values = ignoredTag.getList(key, Tag.TAG_STRING);
Set<UUID> ignored = new HashSet<>();
for (Tag value : values) {
ignored.add(UUID.fromString(value.getAsString()));
}
data.ignoredPlayers.put(UUID.fromString(key), ignored);
}
return data;
}
public boolean isTradeEnabled(UUID playerId) {
return tradeEnabled.getOrDefault(playerId, true);
}
public void setTradeEnabled(UUID playerId, boolean enabled) {
tradeEnabled.put(playerId, enabled);
setDirty();
}
public boolean isIgnoring(UUID playerId, UUID ignoredPlayerId) {
return ignoredPlayers.getOrDefault(playerId, Set.of()).contains(ignoredPlayerId);
}
public boolean addIgnoredPlayer(UUID playerId, UUID ignoredPlayerId) {
boolean changed = ignoredPlayers.computeIfAbsent(playerId, ignored -> new HashSet<>()).add(ignoredPlayerId);
if (changed) {
setDirty();
}
return changed;
}
public boolean removeIgnoredPlayer(UUID playerId, UUID ignoredPlayerId) {
Set<UUID> ignored = ignoredPlayers.get(playerId);
if (ignored == null) {
return false;
}
boolean changed = ignored.remove(ignoredPlayerId);
if (ignored.isEmpty()) {
ignoredPlayers.remove(playerId);
}
if (changed) {
setDirty();
}
return changed;
}
public List<UUID> ignoredPlayers(UUID playerId) {
return new ArrayList<>(ignoredPlayers.getOrDefault(playerId, Set.of()));
}
@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
CompoundTag enabledTag = new CompoundTag();
for (Map.Entry<UUID, Boolean> entry : tradeEnabled.entrySet()) {
enabledTag.putBoolean(entry.getKey().toString(), entry.getValue());
}
tag.put("trade_enabled", enabledTag);
CompoundTag ignoredTag = new CompoundTag();
for (Map.Entry<UUID, Set<UUID>> entry : ignoredPlayers.entrySet()) {
ListTag list = new ListTag();
for (UUID ignored : entry.getValue()) {
list.add(StringTag.valueOf(ignored.toString()));
}
ignoredTag.put(entry.getKey().toString(), list);
}
tag.put("ignored_players", ignoredTag);
return tag;
}
}