Compare commits
10 Commits
256901ff00
...
61063bca15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61063bca15 | ||
|
|
84ff3b9474 | ||
|
|
4afd604605 | ||
|
|
afcec3d799 | ||
|
|
9bcc0dc6fe | ||
|
|
786d78e6fe | ||
|
|
31b1c6b6bb | ||
|
|
a4474870a0 | ||
|
|
cf97ebe11b | ||
|
|
19a85628f2 |
66
.github/workflows/build.yml
vendored
66
.github/workflows/build.yml
vendored
@@ -1,26 +1,56 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: modrinth
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '25'
|
||||
distribution: 'temurin'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Execute Gradle build (all Stonecutter targets)
|
||||
run: ./gradlew chiseledBuild
|
||||
- run: mkdir staging && cp build/libs/*.jar staging
|
||||
- run: cd build/libs && md5sum *.jar > ../../md5sum.txt
|
||||
- run: echo "filename=`ls build/libs/*.jar |xargs basename`" >> $GITHUB_ENV
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: build/libs/*.jar
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build all Stonecutter targets
|
||||
run: ./gradlew chiseledBuild --no-daemon --stacktrace
|
||||
|
||||
- name: Collect remapped jars (per Minecraft version)
|
||||
run: |
|
||||
mkdir -p staging
|
||||
find versions -path '*/build/libs/*.jar' \
|
||||
! -name '*-sources.jar' \
|
||||
! -name '*-dev.jar' \
|
||||
-exec cp -v {} staging/ \;
|
||||
if [ -z "$(ls -A staging 2>/dev/null)" ]; then
|
||||
echo "No jars found under versions/*/build/libs/"
|
||||
exit 1
|
||||
fi
|
||||
(cd staging && md5sum *.jar) | tee md5sum.txt
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jars
|
||||
path: |
|
||||
staging/*.jar
|
||||
md5sum.txt
|
||||
|
||||
- name: Release (GitHub tags only)
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
staging/*.jar
|
||||
md5sum.txt
|
||||
|
||||
@@ -3,7 +3,7 @@ import java.time.format.DateTimeFormatter
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id("dev.isxander.modstitch.base") version "0.8.4"
|
||||
id("dev.isxander.modstitch.base") version "0.8.5"
|
||||
id("com.modrinth.minotaur") version "2.+"
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ stonecutter {
|
||||
"forge" to loader.equals("forge", ignoreCase = true),
|
||||
"vanilla" to loader.equals("vanilla", ignoreCase = true),
|
||||
"mc26" to (minecraft == "26.1.2"),
|
||||
// TraderHighlightRenderer: MC 26 uses Fabric LevelRenderEvents; 1.21.x uses WorldRenderEvents (v1.world).
|
||||
"traderWireframeRender" to (minecraft == "26.1.2" || minecraft == "1.21.11" || minecraft == "1.21.10"),
|
||||
"traderWireframeMc26" to (minecraft == "26.1.2"),
|
||||
"traderWireframe121" to (minecraft == "1.21.10" || minecraft == "1.21.11"),
|
||||
"traderWireframe12110" to (minecraft == "1.21.10"),
|
||||
"traderWireframe12111" to (minecraft == "1.21.11"),
|
||||
"npcSplit" to (minecraft == "26.1.2" || minecraft == "1.21.11"),
|
||||
"npcFlat" to (minecraft == "1.21.10"),
|
||||
)
|
||||
|
||||
@@ -10,4 +10,4 @@ mod_group=com.github.sebseb7.autotrade
|
||||
author=sebseb7
|
||||
mod_file_name=autotrade-fabric
|
||||
|
||||
mod_version=0.0.14
|
||||
mod_version=0.0.16
|
||||
|
||||
@@ -11,6 +11,13 @@ public class AutoTrade implements ModInitializer {
|
||||
public static int sold = 0;
|
||||
public static int bought = 0;
|
||||
|
||||
/**
|
||||
* Set to {@code true} while the auto-trader is firing its synthetic
|
||||
* villager interact packets so {@link InitHandler}'s {@code UseEntityCallback}
|
||||
* can tell automated clicks apart from a real player right-click.
|
||||
*/
|
||||
public static boolean autoInteracting = false;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
InitializationHandler.getInstance().registerInitializationHandler(new InitHandler());
|
||||
|
||||
@@ -5,13 +5,24 @@ import com.github.sebseb7.autotrade.event.InputHandler;
|
||||
import com.github.sebseb7.autotrade.event.KeybindCallbacks;
|
||||
import com.github.sebseb7.autotrade.gui.MerchantScreenButtonInjector;
|
||||
import com.github.sebseb7.autotrade.render.TraderHighlightRenderer;
|
||||
import com.github.sebseb7.autotrade.render.VillagerTradeOverlayRenderer;
|
||||
import fi.dy.masa.malilib.config.ConfigManager;
|
||||
import fi.dy.masa.malilib.config.options.ConfigString;
|
||||
import fi.dy.masa.malilib.event.InputEventHandler;
|
||||
import fi.dy.masa.malilib.event.TickHandler;
|
||||
import fi.dy.masa.malilib.gui.Message;
|
||||
import fi.dy.masa.malilib.interfaces.IInitializationHandler;
|
||||
import fi.dy.masa.malilib.interfaces.IValueChangeCallback;
|
||||
import fi.dy.masa.malilib.util.InfoUtils;
|
||||
import net.fabricmc.fabric.api.event.player.UseEntityCallback;
|
||||
//? if npcSplit {
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
|
||||
//?}
|
||||
//? if npcFlat {
|
||||
import net.minecraft.world.entity.npc.Villager;
|
||||
import net.minecraft.world.entity.npc.WanderingTrader;
|
||||
//?}
|
||||
import net.minecraft.world.InteractionResult;
|
||||
|
||||
public class InitHandler implements IInitializationHandler {
|
||||
@Override
|
||||
@@ -19,7 +30,6 @@ public class InitHandler implements IInitializationHandler {
|
||||
ConfigManager.getInstance().registerConfigHandler(Reference.MOD_ID, new Configs());
|
||||
|
||||
TraderHighlightRenderer.register();
|
||||
VillagerTradeOverlayRenderer.register();
|
||||
MerchantScreenButtonInjector.register();
|
||||
|
||||
InputHandler handler = new InputHandler();
|
||||
@@ -29,6 +39,20 @@ public class InitHandler implements IInitializationHandler {
|
||||
|
||||
KeybindCallbacks.getInstance().setCallbacks();
|
||||
|
||||
// A real right-click on a villager/wandering trader cancels the global
|
||||
// auto-trade switch — but skip the synthetic interact packets the mod
|
||||
// itself emits in AutoTradeClientTick (guarded by AutoTrade.autoInteracting).
|
||||
UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> {
|
||||
if (!AutoTrade.autoInteracting && player.level().isClientSide()
|
||||
&& (entity instanceof Villager || entity instanceof WanderingTrader)
|
||||
&& Configs.Generic.ENABLED.getBooleanValue()) {
|
||||
Configs.Generic.ENABLED.setBooleanValue(false);
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.toggled_mod_off");
|
||||
Configs.saveToFile();
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
});
|
||||
|
||||
ValueChangeCallback valueChangeCallback = new ValueChangeCallback();
|
||||
Configs.Generic.SELL_ITEM.setValueChangeCallback(valueChangeCallback);
|
||||
Configs.Generic.BUY_ITEM.setValueChangeCallback(valueChangeCallback);
|
||||
|
||||
@@ -27,10 +27,7 @@ public class Configs implements IConfigHandler {
|
||||
"Do auto trading with villagers in range");
|
||||
public static final ConfigBoolean ITEM_FRAME = new ConfigBoolean("selectUsingItemFrame", true,
|
||||
"Select buy/sell items with item frames (max. distance 3) with items nametagged \"buy\" or \"sell\"");
|
||||
public static final ConfigBoolean GLASS_BLOCK = new ConfigBoolean("selectUsingGlassBlock", false,
|
||||
"Select input and output containers by placing red (input) and blue (output) stained glass blocks <selectionBlockOffset> blocks above them (or below if negative)");
|
||||
public static final ConfigInteger SELECTOR_OFFSET = new ConfigInteger("selectionBlockOffset", 3, -10, 10,
|
||||
"x Blocks below a red stained glass block will be input container, x Blocks below a blue stained glass block will be output container (or above, if x is negative)");
|
||||
|
||||
public static final ConfigBoolean ENABLE_SELL = new ConfigBoolean("enableSell", false,
|
||||
"Enable selling (if disabled emeralds are taken from the input container)");
|
||||
public static final ConfigString SELL_ITEM = new ConfigString("sellItem", "minecraft:gold_ingot",
|
||||
@@ -63,16 +60,15 @@ public class Configs implements IConfigHandler {
|
||||
"true: Start the delay after the villager was unloaded; false: Start the delay after the trade has been initiated");
|
||||
public static final ConfigInteger CONTAINER_CLOSE_DELAY = new ConfigInteger("containerCloseDelay", 0, 0,
|
||||
30000000, "delay in ticks; to get signal from trapped chest");
|
||||
public static final ConfigBoolean SHOW_TRADES = new ConfigBoolean("showTrades", true,
|
||||
"Display villager/wandering-trader trades above their heads (requires trading with them once to cache the offers)");
|
||||
|
||||
public static final ConfigString SELECTED_ENCHANTMENTS = new ConfigString("selectedEnchantments", "",
|
||||
"Comma-separated list of selected enchantment IDs (set via the \"Select Enchantments\" button on a librarian's trade screen)");
|
||||
|
||||
public static final ImmutableList<IConfigValue> OPTIONS = ImmutableList.of(ENABLED, ITEM_FRAME, GLASS_BLOCK,
|
||||
SELECTOR_OFFSET, ENABLE_SELL, SELL_ITEM, SELL_LIMIT, ENABLE_BUY, BUY_ITEM, BUY_LIMIT, MAX_INPUT_ITEMS,
|
||||
public static final ImmutableList<IConfigValue> OPTIONS = ImmutableList.of(ENABLED, ITEM_FRAME,
|
||||
ENABLE_SELL, SELL_ITEM, SELL_LIMIT, ENABLE_BUY, BUY_ITEM, BUY_LIMIT, MAX_INPUT_ITEMS,
|
||||
INPUT_CONTAINER_X, INPUT_CONTAINER_Y, INPUT_CONTAINER_Z, OUTPUT_CONTAINER_X, OUTPUT_CONTAINER_Y,
|
||||
OUTPUT_CONTAINER_Z, VOID_TRADING_DELAY, VOID_TRADING_DELAY_AFTER_TELEPORT, CONTAINER_CLOSE_DELAY,
|
||||
SHOW_TRADES, SELECTED_ENCHANTMENTS);
|
||||
SELECTED_ENCHANTMENTS);
|
||||
}
|
||||
|
||||
public static void loadFromFile() {
|
||||
|
||||
@@ -1,487 +1,71 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.AutoTrade;
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import com.github.sebseb7.autotrade.render.VillagerTradeCache;
|
||||
import com.github.sebseb7.autotrade.util.TradeItemSpec;
|
||||
import fi.dy.masa.malilib.gui.Message;
|
||||
import fi.dy.masa.malilib.util.GuiUtils;
|
||||
import fi.dy.masa.malilib.util.InfoUtils;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.inventory.ContainerScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.ShulkerBoxScreen;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.protocol.game.ServerboundSelectTradePacket;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
//? if npcSplit {
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
|
||||
//?}
|
||||
//? if npcFlat {
|
||||
import net.minecraft.world.entity.npc.Villager;
|
||||
import net.minecraft.world.entity.npc.WanderingTrader;
|
||||
//?}
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.MerchantMenu;
|
||||
import net.minecraft.world.inventory.ShulkerBoxMenu;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.trading.MerchantOffer;
|
||||
import net.minecraft.world.item.trading.MerchantOffers;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
//? if mc26 {
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
//?}
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Main tick handler for AutoTrade.
|
||||
* Orchestrates trader location, merchant trading, and container management.
|
||||
*/
|
||||
final class AutoTradeClientTick {
|
||||
private final Vector<Entity> villagersInRange = new Vector<>();
|
||||
private int villagerActive = 0;
|
||||
private final AutoTradeTickState state = new AutoTradeTickState();
|
||||
private final TraderInteractor traderInteractor = new TraderInteractor(state);
|
||||
private final AutoTradeContainerFlow containerFlow = new AutoTradeContainerFlow(state);
|
||||
private final AutoTradeMerchantScreenTick merchantScreenTick = new AutoTradeMerchantScreenTick(state);
|
||||
|
||||
private boolean state = false;
|
||||
private boolean inputInRange = false;
|
||||
private boolean inputOpened = false;
|
||||
private boolean outputInRange = false;
|
||||
private boolean outputOpened = false;
|
||||
private int tickCount = 0;
|
||||
private int voidDelay = 0;
|
||||
private int containerDelay = 0;
|
||||
|
||||
/**
|
||||
* 1 second at 20 TPS — client wireframe highlight (see
|
||||
* {@code TraderHighlightRenderer}).
|
||||
*/
|
||||
private static final int TRADER_HIGHLIGHT_TICKS = 20;
|
||||
|
||||
private int traderGlowTicksRemaining = 0;
|
||||
private int traderGlowEntityId = -1;
|
||||
|
||||
private int inputContainerHighlightTicks = 0;
|
||||
private int outputContainerHighlightTicks = 0;
|
||||
|
||||
/**
|
||||
* Entity to draw in-world highlight for; {@code null} when inactive or unknown
|
||||
* id.
|
||||
*/
|
||||
Entity getTraderGlowEntityForRender(Minecraft mc) {
|
||||
if (traderGlowTicksRemaining <= 0 || traderGlowEntityId < 0 || mc.level == null) {
|
||||
return null;
|
||||
}
|
||||
return findEntityById(mc, traderGlowEntityId);
|
||||
}
|
||||
|
||||
void tick(Minecraft mc) {
|
||||
tickTraderGlow(mc);
|
||||
tickContainerHighlights(mc);
|
||||
if (voidDelay > 0) {
|
||||
if (Configs.Generic.VOID_TRADING_DELAY_AFTER_TELEPORT.getBooleanValue()) {
|
||||
boolean found = false;
|
||||
for (Entity entity : mc.level.entitiesForRendering()) {
|
||||
if (entity.getId() == villagerActive) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
voidDelay--;
|
||||
}
|
||||
} else {
|
||||
voidDelay--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (containerDelay > 0) {
|
||||
containerDelay--;
|
||||
}
|
||||
if (!Configs.Generic.ENABLED.getBooleanValue() || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
Inventory plInv = mc.player.getInventory();
|
||||
if (Configs.Generic.GLASS_BLOCK.getBooleanValue()) {
|
||||
tickGlassBlockSelection(mc);
|
||||
}
|
||||
if (Configs.Generic.ITEM_FRAME.getBooleanValue()) {
|
||||
tickItemFrameSelection(mc);
|
||||
}
|
||||
if (GuiUtils.getCurrentScreen() instanceof MerchantScreen screen) {
|
||||
tickMerchantScreen(mc, screen);
|
||||
inputInRange = false;
|
||||
outputInRange = false;
|
||||
return;
|
||||
}
|
||||
if (GuiUtils.getCurrentScreen() instanceof ShulkerBoxScreen sbs) {
|
||||
ShulkerBoxMenu m = sbs.getMenu();
|
||||
if ((containerDelay == 0) && inputOpened) {
|
||||
inputOpened = false;
|
||||
ContainerIoHelper.processInput(m, plInv);
|
||||
sbs.onClose();
|
||||
}
|
||||
if ((containerDelay == 0) && outputOpened) {
|
||||
outputOpened = false;
|
||||
ContainerIoHelper.processOutput(m, plInv);
|
||||
sbs.onClose();
|
||||
}
|
||||
} else if (GuiUtils.getCurrentScreen() instanceof ContainerScreen cs) {
|
||||
AbstractContainerMenu m = cs.getMenu();
|
||||
if ((containerDelay == 0) && inputOpened) {
|
||||
inputOpened = false;
|
||||
ContainerIoHelper.processInput(m, plInv);
|
||||
cs.onClose();
|
||||
}
|
||||
if ((containerDelay == 0) && outputOpened) {
|
||||
outputOpened = false;
|
||||
ContainerIoHelper.processOutput(m, plInv);
|
||||
cs.onClose();
|
||||
}
|
||||
}
|
||||
boolean found = false;
|
||||
Vector<Entity> newVillagersInRange = new Vector<>(villagersInRange);
|
||||
for (Entity entity : mc.level.entitiesForRendering()) {
|
||||
if (entity instanceof Villager || entity instanceof WanderingTrader) {
|
||||
if (entity.distanceToSqr(mc.player) < (2.5f * 2.5f)) {
|
||||
if (!found) {
|
||||
if (!newVillagersInRange.contains(entity)) {
|
||||
found = true;
|
||||
newVillagersInRange.add(entity);
|
||||
//? if mc26 {
|
||||
EntityHitResult ehr = new EntityHitResult(entity, entity.position());
|
||||
mc.gameMode.interact(mc.player, entity, ehr, InteractionHand.MAIN_HAND);
|
||||
//?} else {
|
||||
mc.gameMode.interact(mc.player, entity, InteractionHand.MAIN_HAND);
|
||||
//?}
|
||||
voidDelay = Configs.Generic.VOID_TRADING_DELAY.getIntegerValue();
|
||||
villagerActive = entity.getId();
|
||||
state = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Entity entity : villagersInRange) {
|
||||
if (entity.distanceToSqr(mc.player) >= 16.0D) {
|
||||
newVillagersInRange.remove(entity);
|
||||
}
|
||||
}
|
||||
villagersInRange.clear();
|
||||
villagersInRange.addAll(newVillagersInRange);
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
BlockPos input = new BlockPos(Configs.Generic.INPUT_CONTAINER_X.getIntegerValue(),
|
||||
Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue(),
|
||||
Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue());
|
||||
BlockPos output = new BlockPos(Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue(),
|
||||
Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue(),
|
||||
Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue());
|
||||
Vec3 ic = input.getCenter();
|
||||
Vec3 oc = output.getCenter();
|
||||
if ((mc.player.distanceToSqr(ic) < 16.0D) && (inputInRange == false)) {
|
||||
inputInRange = true;
|
||||
mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(ic, Direction.UP, input, false));
|
||||
containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue();
|
||||
inputOpened = true;
|
||||
inputContainerHighlightTicks = TRADER_HIGHLIGHT_TICKS;
|
||||
return;
|
||||
}
|
||||
if ((mc.player.distanceToSqr(oc) < 16.0D) && (outputInRange == false)) {
|
||||
outputInRange = true;
|
||||
mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(oc, Direction.UP, output, false));
|
||||
containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue();
|
||||
outputOpened = true;
|
||||
outputContainerHighlightTicks = TRADER_HIGHLIGHT_TICKS;
|
||||
return;
|
||||
}
|
||||
if (mc.player.distanceToSqr(ic) > 25.0D) {
|
||||
inputOpened = false;
|
||||
inputInRange = false;
|
||||
}
|
||||
if (mc.player.distanceToSqr(oc) > 25.0D) {
|
||||
outputOpened = false;
|
||||
outputInRange = false;
|
||||
}
|
||||
tickCount++;
|
||||
if (tickCount > 200) {
|
||||
tickCount = 0;
|
||||
inputInRange = false;
|
||||
outputInRange = false;
|
||||
var cur = GuiUtils.getCurrentScreen();
|
||||
if (cur != null) {
|
||||
if (cur instanceof MerchantScreen || cur instanceof ShulkerBoxScreen
|
||||
|| cur instanceof ContainerScreen) {
|
||||
cur.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickGlassBlockSelection(Minecraft mc) {
|
||||
int playerX = (int) mc.player.getX();
|
||||
int playerZ = (int) mc.player.getZ();
|
||||
int playerY = (int) mc.player.getY();
|
||||
int selectorOffset = Configs.Generic.SELECTOR_OFFSET.getIntegerValue();
|
||||
int absSelectorOffset = Math.abs(selectorOffset);
|
||||
for (int x = playerX - (absSelectorOffset + 3); x < playerX + (absSelectorOffset + 3); x += 1) {
|
||||
for (int z = playerZ - (absSelectorOffset + 3); z < playerZ + (absSelectorOffset + 3); z += 1) {
|
||||
for (int y = playerY - (absSelectorOffset + 3); y < playerY + (absSelectorOffset + 3); y += 1) {
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
if (mc.level.getBlockState(pos).getBlock() == Blocks.RED_STAINED_GLASS) {
|
||||
if ((x != Configs.Generic.INPUT_CONTAINER_X.getIntegerValue())
|
||||
|| ((y - selectorOffset) != Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue())
|
||||
|| (z != Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue())) {
|
||||
Configs.Generic.INPUT_CONTAINER_X.setIntegerValue(x);
|
||||
Configs.Generic.INPUT_CONTAINER_Y.setIntegerValue(y - selectorOffset);
|
||||
Configs.Generic.INPUT_CONTAINER_Z.setIntegerValue(z);
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO,
|
||||
"autotrade.message.input_container_set", x, y - selectorOffset, z);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mc.level.getBlockState(pos).getBlock() == Blocks.BLUE_STAINED_GLASS) {
|
||||
if ((x != Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue())
|
||||
|| ((y - selectorOffset) != Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue())
|
||||
|| (z != Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue())) {
|
||||
Configs.Generic.OUTPUT_CONTAINER_X.setIntegerValue(x);
|
||||
Configs.Generic.OUTPUT_CONTAINER_Y.setIntegerValue(y - selectorOffset);
|
||||
Configs.Generic.OUTPUT_CONTAINER_Z.setIntegerValue(z);
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO,
|
||||
"autotrade.message.output_container_set", x, y - selectorOffset, z);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickItemFrameSelection(Minecraft mc) {
|
||||
Vec3 pm = new Vec3(mc.player.getX(), mc.player.getY(), mc.player.getZ());
|
||||
AABB box = new AABB(pm.subtract(3, 3, 3), pm.add(3, 3, 3));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<ItemFrame> frames = (List<ItemFrame>) (List<?>) mc.level.getEntities((Entity) null, box,
|
||||
e -> e instanceof ItemFrame && e.isAlive());
|
||||
for (ItemFrame entity : frames) {
|
||||
ItemStack stack = entity.getItem();
|
||||
String customName = stack.getHoverName().getString();
|
||||
if ("sell".equalsIgnoreCase(customName) || "\"sell\"".equals(customName)) {
|
||||
String sellItem = TradeItemSpec.encodeFromStack(stack);
|
||||
if (!Configs.Generic.SELL_ITEM.getStringValue().equals(sellItem)) {
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set",
|
||||
sellItem);
|
||||
Configs.Generic.SELL_ITEM.setValueFromString(sellItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ("buy".equalsIgnoreCase(customName) || "\"buy\"".equals(customName)) {
|
||||
String buyItem = TradeItemSpec.encodeFromStack(stack);
|
||||
if (!Configs.Generic.BUY_ITEM.getStringValue().equals(buyItem)) {
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set",
|
||||
buyItem);
|
||||
Configs.Generic.BUY_ITEM.setValueFromString(buyItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickMerchantScreen(Minecraft mc, MerchantScreen screen) {
|
||||
if (!state) {
|
||||
String sellItemStr = Configs.Generic.SELL_ITEM.getStringValue();
|
||||
String buyItemStr = Configs.Generic.BUY_ITEM.getStringValue();
|
||||
state = true;
|
||||
MerchantMenu menu = screen.getMenu();
|
||||
MerchantOffers offers = menu.getOffers();
|
||||
|
||||
// Cache offers for the in-world trade overlay.
|
||||
Entity activeEntity = findEntityById(mc, villagerActive);
|
||||
if (activeEntity != null && offers != null && !offers.isEmpty()) {
|
||||
VillagerTradeCache.put(activeEntity.getUUID(), offers);
|
||||
}
|
||||
for (int i = 0; i < offers.size(); i++) {
|
||||
MerchantOffer offer = offers.get(i);
|
||||
int tradesLeft = offer.getMaxUses() - offer.getUses();
|
||||
if (TradeItemSpec.matches(offer.getResult(), buyItemStr) && Configs.Generic.ENABLE_BUY.getBooleanValue()
|
||||
&& offer.getResult().getCount() <= Configs.Generic.BUY_LIMIT.getIntegerValue()) {
|
||||
if (tradesLeft > 0 && playerHasMerchantCosts(mc.player, offer)) {
|
||||
Slot slot = menu.getSlot(2);
|
||||
menu.setSelectionHint(i);
|
||||
mc.player.connection.send(new ServerboundSelectTradePacket(i));
|
||||
AutoTrade.bought += offer.getMaxUses();
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.trade_bought",
|
||||
formatItemCountNameForTrades(offer.getResult(), tradesLeft),
|
||||
formatOfferPriceForTrades(offer, tradesLeft));
|
||||
try {
|
||||
ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index);
|
||||
} catch (Exception e) {
|
||||
System.out.println("err " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TradeItemSpec.matches(offer.getCostA(), sellItemStr)
|
||||
&& Configs.Generic.ENABLE_SELL.getBooleanValue()
|
||||
&& offer.getCostA().getCount() <= Configs.Generic.SELL_LIMIT.getIntegerValue()) {
|
||||
if (tradesLeft > 0 && playerHasMerchantCosts(mc.player, offer)) {
|
||||
Slot slot = menu.getSlot(2);
|
||||
menu.setSelectionHint(i);
|
||||
AutoTrade.sold += offer.getMaxUses();
|
||||
mc.player.connection.send(new ServerboundSelectTradePacket(i));
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.trade_sold",
|
||||
formatItemCountNameForTrades(offer.getCostA(), tradesLeft)
|
||||
+ formatOptionalSecondCostForTrades(offer, tradesLeft),
|
||||
formatItemCountNameForTrades(offer.getResult(), tradesLeft));
|
||||
try {
|
||||
ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index);
|
||||
} catch (Exception e) {
|
||||
System.out.println("err " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
screen.onClose();
|
||||
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
|
||||
startTraderGlow(mc, villagerActive);
|
||||
}
|
||||
|
||||
private void tickTraderGlow(Minecraft mc) {
|
||||
if (mc.level == null || traderGlowTicksRemaining <= 0) {
|
||||
return;
|
||||
}
|
||||
traderGlowTicksRemaining--;
|
||||
if (traderGlowTicksRemaining == 0) {
|
||||
traderGlowEntityId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void tickContainerHighlights(Minecraft mc) {
|
||||
if (mc.level == null) {
|
||||
return;
|
||||
}
|
||||
if (inputContainerHighlightTicks > 0) {
|
||||
inputContainerHighlightTicks--;
|
||||
}
|
||||
if (outputContainerHighlightTicks > 0) {
|
||||
outputContainerHighlightTicks--;
|
||||
}
|
||||
return state.getTraderGlowEntityForRender(mc);
|
||||
}
|
||||
|
||||
int getInputContainerHighlightTicks() {
|
||||
return inputContainerHighlightTicks;
|
||||
return state.getInputContainerHighlightTicks();
|
||||
}
|
||||
|
||||
int getOutputContainerHighlightTicks() {
|
||||
return outputContainerHighlightTicks;
|
||||
return state.getOutputContainerHighlightTicks();
|
||||
}
|
||||
|
||||
private void startTraderGlow(Minecraft mc, int entityId) {
|
||||
if (mc.level == null) {
|
||||
void tick(Minecraft mc) {
|
||||
state.tickTraderGlow(mc);
|
||||
state.tickContainerHighlights(mc);
|
||||
|
||||
if (AutoTradeVoidDelay.handle(mc, state)) return;
|
||||
|
||||
if (state.containerDelay > 0) state.containerDelay--;
|
||||
if (mc.player == null) return;
|
||||
|
||||
tickPostMerchantSync(mc);
|
||||
if (!Configs.Generic.ENABLED.getBooleanValue()) {
|
||||
state.clearMerchantQuickMoveDefer();
|
||||
return;
|
||||
}
|
||||
if (findEntityById(mc, entityId) == null) {
|
||||
traderGlowTicksRemaining = 0;
|
||||
traderGlowEntityId = -1;
|
||||
|
||||
AutoTradeConfigSelectors.tickItemFrameSelection(mc);
|
||||
|
||||
merchantScreenTick.tickDeferredResultQuickMove(mc);
|
||||
|
||||
if (GuiUtils.getCurrentScreen() instanceof MerchantScreen screen) {
|
||||
merchantScreenTick.tick(mc, screen);
|
||||
containerFlow.resetContainerFlags();
|
||||
return;
|
||||
}
|
||||
traderGlowEntityId = entityId;
|
||||
traderGlowTicksRemaining = TRADER_HIGHLIGHT_TICKS;
|
||||
|
||||
containerFlow.processOpenContainers(mc, mc.player.getInventory());
|
||||
|
||||
if (traderInteractor.findAndInteract(mc)) return;
|
||||
|
||||
if (containerFlow.handleContainerProximity(mc)) return;
|
||||
|
||||
containerFlow.tickPeriodicReset();
|
||||
}
|
||||
|
||||
private static Entity findEntityById(Minecraft mc, int entityId) {
|
||||
for (Entity e : mc.level.entitiesForRendering()) {
|
||||
if (e.getId() == entityId) {
|
||||
return e;
|
||||
}
|
||||
private void tickPostMerchantSync(Minecraft mc) {
|
||||
if (state.postMerchantInventorySyncTicks > 0) {
|
||||
state.postMerchantInventorySyncTicks--;
|
||||
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same stack rules as the merchant menu: player must have enough of each
|
||||
* non-empty cost before we fire packets or show a trade toast.
|
||||
*/
|
||||
private static boolean playerHasMerchantCosts(Player player, MerchantOffer offer) {
|
||||
if (!costRequirementMet(player.getInventory(), offer.getCostA())) {
|
||||
return false;
|
||||
}
|
||||
return costRequirementMet(player.getInventory(), offer.getCostB());
|
||||
}
|
||||
|
||||
private static boolean costRequirementMet(Inventory inv, ItemStack required) {
|
||||
if (required.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
int need = required.getCount();
|
||||
int have = 0;
|
||||
for (int s = 0; s < inv.getContainerSize(); s++) {
|
||||
ItemStack st = inv.getItem(s);
|
||||
if (ItemStack.isSameItemSameComponents(st, required)) {
|
||||
have += st.getCount();
|
||||
if (have >= need) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** e.g. "3× Book" (one villager use). */
|
||||
private static String formatItemCountAndName(ItemStack stack) {
|
||||
return stack.getCount() + "× " + stack.getHoverName().getString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-trade count × how many of this offer remain before the trade, e.g. 1
|
||||
* iron/trade × 12 runs → "12× …".
|
||||
*/
|
||||
private static String formatItemCountNameForTrades(ItemStack perTrade, int remainingOfferUses) {
|
||||
if (remainingOfferUses <= 0) {
|
||||
return formatItemCountAndName(perTrade);
|
||||
}
|
||||
return (perTrade.getCount() * remainingOfferUses) + "× " + perTrade.getHoverName().getString();
|
||||
}
|
||||
|
||||
/** For buying: the stacks you pay, scaled to how many of this offer remain. */
|
||||
private static String formatOfferPriceForTrades(MerchantOffer offer, int t) {
|
||||
if (t <= 0) {
|
||||
String a = offer.getCostA().isEmpty() ? null : formatItemCountAndName(offer.getCostA());
|
||||
if (offer.getCostB().isEmpty()) {
|
||||
return a != null ? a : "—";
|
||||
}
|
||||
String b = formatItemCountAndName(offer.getCostB());
|
||||
return a == null ? b : a + " + " + b;
|
||||
}
|
||||
String a = offer.getCostA().isEmpty()
|
||||
? null
|
||||
: (offer.getCostA().getCount() * t) + "× " + offer.getCostA().getHoverName().getString();
|
||||
if (offer.getCostB().isEmpty()) {
|
||||
return a != null ? a : "—";
|
||||
}
|
||||
String b = (offer.getCostB().getCount() * t) + "× " + offer.getCostB().getHoverName().getString();
|
||||
return a == null ? b : a + " + " + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the trade has a second cost item, " + 2× …" scaled to remaining offer
|
||||
* uses.
|
||||
*/
|
||||
private static String formatOptionalSecondCostForTrades(MerchantOffer offer, int t) {
|
||||
if (offer.getCostB().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (t <= 0) {
|
||||
return " + " + formatItemCountAndName(offer.getCostB());
|
||||
}
|
||||
return " + " + (offer.getCostB().getCount() * t) + "× " + offer.getCostB().getHoverName().getString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import fi.dy.masa.malilib.gui.Message.MessageType;
|
||||
import fi.dy.masa.malilib.util.InfoUtils;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
final class AutoTradeConfigSelectors {
|
||||
|
||||
private AutoTradeConfigSelectors() {}
|
||||
|
||||
static void tickItemFrameSelection(Minecraft mc) {
|
||||
if (!Configs.Generic.ITEM_FRAME.getBooleanValue()) return;
|
||||
Vec3 pm = new Vec3(mc.player.getX(), mc.player.getY(), mc.player.getZ());
|
||||
var box = new net.minecraft.world.phys.AABB(pm.subtract(3, 3, 3), pm.add(3, 3, 3));
|
||||
@SuppressWarnings("unchecked")
|
||||
var frames = (java.util.List<net.minecraft.world.entity.decoration.ItemFrame>)
|
||||
(java.util.List<?>) mc.level.getEntities((Entity) null, box,
|
||||
e -> e instanceof net.minecraft.world.entity.decoration.ItemFrame && e.isAlive());
|
||||
|
||||
for (var entity : frames) {
|
||||
var stack = entity.getItem();
|
||||
String customName = stack.getHoverName().getString();
|
||||
handleItemFrameSell(stack, customName);
|
||||
handleItemFrameBuy(stack, customName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleItemFrameSell(net.minecraft.world.item.ItemStack stack, String customName) {
|
||||
if (!("sell".equalsIgnoreCase(customName) || "\"sell\"".equals(customName))) return;
|
||||
String sellItem = com.github.sebseb7.autotrade.util.TradeItemSpec.encodeFromStack(stack);
|
||||
if (!Configs.Generic.SELL_ITEM.getStringValue().equals(sellItem)) {
|
||||
InfoUtils.showGuiOrInGameMessage(MessageType.INFO,
|
||||
"autotrade.message.sell_item_set", sellItem);
|
||||
Configs.Generic.SELL_ITEM.setValueFromString(sellItem);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleItemFrameBuy(net.minecraft.world.item.ItemStack stack, String customName) {
|
||||
if (!("buy".equalsIgnoreCase(customName) || "\"buy\"".equals(customName))) return;
|
||||
String buyItem = com.github.sebseb7.autotrade.util.TradeItemSpec.encodeFromStack(stack);
|
||||
if (!Configs.Generic.BUY_ITEM.getStringValue().equals(buyItem)) {
|
||||
InfoUtils.showGuiOrInGameMessage(MessageType.INFO,
|
||||
"autotrade.message.buy_item_set", buyItem);
|
||||
Configs.Generic.BUY_ITEM.setValueFromString(buyItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import fi.dy.masa.malilib.util.GuiUtils;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.inventory.ContainerScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.ShulkerBoxScreen;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
final class AutoTradeContainerFlow {
|
||||
private final AutoTradeTickState state;
|
||||
|
||||
AutoTradeContainerFlow(AutoTradeTickState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
void processOpenContainers(Minecraft mc, Inventory plInv) {
|
||||
if (GuiUtils.getCurrentScreen() instanceof ShulkerBoxScreen sbs) {
|
||||
processContainerIo(sbs, sbs.getMenu(), plInv);
|
||||
} else if (GuiUtils.getCurrentScreen() instanceof ContainerScreen cs) {
|
||||
processContainerIo(cs, cs.getMenu(), plInv);
|
||||
}
|
||||
}
|
||||
|
||||
void resetContainerFlags() {
|
||||
state.inputInRange = false;
|
||||
state.outputInRange = false;
|
||||
}
|
||||
|
||||
boolean handleContainerProximity(Minecraft mc) {
|
||||
BlockPos input = new BlockPos(
|
||||
Configs.Generic.INPUT_CONTAINER_X.getIntegerValue(),
|
||||
Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue(),
|
||||
Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue());
|
||||
BlockPos output = new BlockPos(
|
||||
Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue(),
|
||||
Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue(),
|
||||
Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue());
|
||||
Vec3 ic = input.getCenter();
|
||||
Vec3 oc = output.getCenter();
|
||||
|
||||
if (mc.player.distanceToSqr(ic) < 16.0D && !state.inputInRange) {
|
||||
state.inputInRange = true;
|
||||
mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(ic, net.minecraft.core.Direction.UP, input, false));
|
||||
state.containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue();
|
||||
state.inputOpened = true;
|
||||
state.inputContainerHighlightTicks = AutoTradeTickState.TRADER_HIGHLIGHT_TICKS;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mc.player.distanceToSqr(oc) < 16.0D && !state.outputInRange) {
|
||||
state.outputInRange = true;
|
||||
mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(oc, net.minecraft.core.Direction.UP, output, false));
|
||||
state.containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue();
|
||||
state.outputOpened = true;
|
||||
state.outputContainerHighlightTicks = AutoTradeTickState.TRADER_HIGHLIGHT_TICKS;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mc.player.distanceToSqr(ic) > 25.0D) {
|
||||
state.inputOpened = false;
|
||||
state.inputInRange = false;
|
||||
}
|
||||
if (mc.player.distanceToSqr(oc) > 25.0D) {
|
||||
state.outputOpened = false;
|
||||
state.outputInRange = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void tickPeriodicReset() {
|
||||
state.tickCount++;
|
||||
if (state.tickCount > 200) {
|
||||
state.tickCount = 0;
|
||||
state.inputInRange = false;
|
||||
state.outputInRange = false;
|
||||
var cur = GuiUtils.getCurrentScreen();
|
||||
if (cur != null && isContainerScreen(cur)) {
|
||||
cur.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processContainerIo(Object screen, AbstractContainerMenu menu, Inventory plInv) {
|
||||
boolean closed = false;
|
||||
if (state.containerDelay == 0 && state.inputOpened) {
|
||||
state.inputOpened = false;
|
||||
ContainerIoHelper.processInput(menu, plInv);
|
||||
closeScreen(screen);
|
||||
closed = true;
|
||||
}
|
||||
if (state.containerDelay == 0 && state.outputOpened) {
|
||||
state.outputOpened = false;
|
||||
ContainerIoHelper.processOutput(menu, plInv);
|
||||
if (!closed) closeScreen(screen);
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeScreen(Object screen) {
|
||||
if (screen instanceof MerchantScreen s) s.onClose();
|
||||
else if (screen instanceof ShulkerBoxScreen s) s.onClose();
|
||||
else if (screen instanceof ContainerScreen s) s.onClose();
|
||||
}
|
||||
|
||||
private static boolean isContainerScreen(Object screen) {
|
||||
return screen instanceof MerchantScreen
|
||||
|| screen instanceof ShulkerBoxScreen
|
||||
|| screen instanceof ContainerScreen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.AutoTrade;
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import com.github.sebseb7.autotrade.render.VillagerTradeCache;
|
||||
import com.github.sebseb7.autotrade.util.TradeItemSpec;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.inventory.MerchantMenu;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.trading.MerchantOffers;
|
||||
|
||||
/**
|
||||
* Villager GUI: one trade attempt per {@link #tick}, matching {@link MerchantOffers}
|
||||
* recipe stacks, then deferring a normal pickup (not shift-click) from the result slot when
|
||||
* it is an enchanted book (shift-click would chain other book trades); other results still use shift-click.
|
||||
*/
|
||||
final class AutoTradeMerchantScreenTick {
|
||||
/** First tick after a trade decrements without acting; remaining ticks wait on the server. */
|
||||
private static final int RESULT_QUICK_MOVE_DELAY_TICKS = 3;
|
||||
|
||||
private final AutoTradeTickState state;
|
||||
|
||||
AutoTradeMerchantScreenTick(AutoTradeTickState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
void tickDeferredResultQuickMove(Minecraft mc) {
|
||||
if (state.merchantResultQuickMoveDelayTicks <= 0) {
|
||||
return;
|
||||
}
|
||||
state.merchantResultQuickMoveDelayTicks--;
|
||||
if (state.merchantResultQuickMoveDelayTicks > 0) {
|
||||
return;
|
||||
}
|
||||
if (!(mc.screen instanceof MerchantScreen screen)) {
|
||||
AutoTrade.logger.warn("[AutoTrade merchant] defer execute aborted: current screen is not MerchantScreen");
|
||||
state.clearMerchantQuickMoveDefer();
|
||||
return;
|
||||
}
|
||||
MerchantMenu menu = screen.getMenu();
|
||||
MerchantOffers offers = menu.getOffers();
|
||||
int idx = state.merchantResultQuickMoveOfferIndex;
|
||||
if (offers == null || idx < 0 || idx >= offers.size()) {
|
||||
AutoTrade.logger.warn(
|
||||
"[AutoTrade merchant] defer execute aborted: bad offer index idx={} offersSize={}",
|
||||
idx,
|
||||
offers == null ? -1 : offers.size());
|
||||
state.clearMerchantQuickMoveDefer();
|
||||
return;
|
||||
}
|
||||
menu.setSelectionHint(idx);
|
||||
menu.tryMoveItems(idx);
|
||||
var slot = menu.getSlot(2);
|
||||
ItemStack slot2 = slot.getItem();
|
||||
String buySpec = Configs.Generic.BUY_ITEM.getStringValue();
|
||||
try {
|
||||
if (state.merchantResultQuickMoveIsBuy) {
|
||||
boolean match = !slot2.isEmpty() && TradeItemSpec.matches(slot2, buySpec);
|
||||
if (match) {
|
||||
ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index);
|
||||
} else {
|
||||
AutoTrade.logger.warn("[AutoTrade merchant] defer quickMove skipped: slot2 did not match buy spec");
|
||||
}
|
||||
} else {
|
||||
if (!slot2.isEmpty()) {
|
||||
ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index);
|
||||
} else {
|
||||
AutoTrade.logger.warn("[AutoTrade merchant] defer quickMove skipped: result slot empty");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
AutoTrade.logger.warn("[AutoTrade merchant] defer quickMove exception", e);
|
||||
}
|
||||
state.clearMerchantQuickMoveDefer();
|
||||
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
|
||||
}
|
||||
|
||||
void tick(Minecraft mc, MerchantScreen screen) {
|
||||
MerchantMenu menu = screen.getMenu();
|
||||
MerchantOffers offers = menu.getOffers();
|
||||
int villagerActive = state.getVillagerActive();
|
||||
|
||||
cacheTraderOffers(mc, villagerActive, offers);
|
||||
|
||||
if (state.merchantResultQuickMoveDelayTicks > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryExecuteOneMerchantTrade(mc, menu, offers)) {
|
||||
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
|
||||
return;
|
||||
}
|
||||
|
||||
finishMerchantSession(mc, screen);
|
||||
}
|
||||
|
||||
private static void cacheTraderOffers(Minecraft mc, int villagerActive, MerchantOffers offers) {
|
||||
Entity activeEntity = TraderInteractor.findEntityById(mc, villagerActive);
|
||||
if (activeEntity != null && offers != null && !offers.isEmpty()) {
|
||||
VillagerTradeCache.put(activeEntity.getUUID(), offers);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryExecuteOneMerchantTrade(Minecraft mc, MerchantMenu menu, MerchantOffers offers) {
|
||||
if (offers == null || offers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String sellItemStr = Configs.Generic.SELL_ITEM.getStringValue();
|
||||
String buyItemStr = Configs.Generic.BUY_ITEM.getStringValue();
|
||||
boolean buyOn = Configs.Generic.ENABLE_BUY.getBooleanValue();
|
||||
boolean sellOn = Configs.Generic.ENABLE_SELL.getBooleanValue();
|
||||
int buyLimit = Configs.Generic.BUY_LIMIT.getIntegerValue();
|
||||
int sellLimit = Configs.Generic.SELL_LIMIT.getIntegerValue();
|
||||
|
||||
for (int i = 0; i < offers.size(); i++) {
|
||||
var offer = offers.get(i);
|
||||
int tradesLeft = offer.getMaxUses() - offer.getUses();
|
||||
boolean buyRecipe = buyOn && TradeItemSpec.matches(offer.getResult(), buyItemStr);
|
||||
boolean buyCountOk = offer.getResult().getCount() <= buyLimit;
|
||||
boolean costOk = TradeFormatHelper.playerHasMerchantCosts(mc.player, offer);
|
||||
|
||||
boolean sellRecipe = sellOn && TradeItemSpec.matches(offer.getCostA(), sellItemStr);
|
||||
boolean sellCountOk = offer.getCostA().getCount() <= sellLimit;
|
||||
|
||||
if (buyOn && buyRecipe && buyCountOk) {
|
||||
if (tradesLeft > 0 && costOk) {
|
||||
menu.setSelectionHint(i);
|
||||
mc.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSelectTradePacket(i));
|
||||
AutoTrade.bought += offer.getMaxUses();
|
||||
TradeFormatHelper.showTradeNotice(mc, "autotrade.message.trade_bought",
|
||||
Component.literal(TradeFormatHelper.formatItemCountNameForTrades(offer.getResult(), tradesLeft)),
|
||||
Component.literal(TradeFormatHelper.formatOfferPriceForTrades(offer, tradesLeft)));
|
||||
state.merchantResultQuickMoveDelayTicks = RESULT_QUICK_MOVE_DELAY_TICKS;
|
||||
state.merchantResultQuickMoveOfferIndex = i;
|
||||
state.merchantResultQuickMoveIsBuy = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sellOn && sellRecipe && sellCountOk) {
|
||||
if (tradesLeft > 0 && costOk) {
|
||||
menu.setSelectionHint(i);
|
||||
mc.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSelectTradePacket(i));
|
||||
AutoTrade.sold += offer.getMaxUses();
|
||||
TradeFormatHelper.showTradeNotice(mc, "autotrade.message.trade_sold",
|
||||
Component.literal(TradeFormatHelper.formatItemCountNameForTrades(offer.getCostA(), tradesLeft)
|
||||
+ TradeFormatHelper.formatOptionalSecondCostForTrades(offer, tradesLeft)),
|
||||
Component.literal(TradeFormatHelper.formatItemCountNameForTrades(offer.getResult(), tradesLeft)));
|
||||
state.merchantResultQuickMoveDelayTicks = RESULT_QUICK_MOVE_DELAY_TICKS;
|
||||
state.merchantResultQuickMoveOfferIndex = i;
|
||||
state.merchantResultQuickMoveIsBuy = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void finishMerchantSession(Minecraft mc, MerchantScreen screen) {
|
||||
state.clearMerchantQuickMoveDefer();
|
||||
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
|
||||
screen.onClose();
|
||||
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
|
||||
state.postMerchantInventorySyncTicks = 15;
|
||||
state.startTraderGlow(mc, state.getVillagerActive());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
/**
|
||||
* Mutable per-session tick state for {@link AutoTradeClientTick}.
|
||||
*/
|
||||
final class AutoTradeTickState {
|
||||
static final int TRADER_HIGHLIGHT_TICKS = 20;
|
||||
|
||||
int tickCount;
|
||||
int voidDelay;
|
||||
int containerDelay;
|
||||
int postMerchantInventorySyncTicks;
|
||||
|
||||
/** Last villager/wandering trader we opened trades with (entity id). */
|
||||
int villagerActive;
|
||||
|
||||
boolean inputInRange;
|
||||
boolean inputOpened;
|
||||
boolean outputInRange;
|
||||
boolean outputOpened;
|
||||
|
||||
int traderGlowTicksRemaining;
|
||||
int traderGlowEntityId = -1;
|
||||
|
||||
int inputContainerHighlightTicks;
|
||||
int outputContainerHighlightTicks;
|
||||
|
||||
/**
|
||||
* After selecting a trade on the server, wait this many client ticks before
|
||||
* shift-moving the result so slot contents match the server.
|
||||
*/
|
||||
int merchantResultQuickMoveDelayTicks;
|
||||
int merchantResultQuickMoveOfferIndex = -1;
|
||||
boolean merchantResultQuickMoveIsBuy;
|
||||
|
||||
void clearMerchantQuickMoveDefer() {
|
||||
merchantResultQuickMoveDelayTicks = 0;
|
||||
merchantResultQuickMoveOfferIndex = -1;
|
||||
}
|
||||
|
||||
int getVillagerActive() {
|
||||
return villagerActive;
|
||||
}
|
||||
|
||||
Entity getTraderGlowEntityForRender(Minecraft mc) {
|
||||
if (traderGlowTicksRemaining <= 0 || traderGlowEntityId < 0 || mc.level == null) return null;
|
||||
return TraderInteractor.findEntityById(mc, traderGlowEntityId);
|
||||
}
|
||||
|
||||
int getInputContainerHighlightTicks() {
|
||||
return inputContainerHighlightTicks;
|
||||
}
|
||||
|
||||
int getOutputContainerHighlightTicks() {
|
||||
return outputContainerHighlightTicks;
|
||||
}
|
||||
|
||||
void tickTraderGlow(Minecraft mc) {
|
||||
if (mc.level == null || traderGlowTicksRemaining <= 0) return;
|
||||
traderGlowTicksRemaining--;
|
||||
if (traderGlowTicksRemaining == 0) traderGlowEntityId = -1;
|
||||
}
|
||||
|
||||
void startTraderGlow(Minecraft mc, int entityId) {
|
||||
if (mc.level == null) {
|
||||
traderGlowTicksRemaining = 0;
|
||||
traderGlowEntityId = -1;
|
||||
return;
|
||||
}
|
||||
Entity active = TraderInteractor.findEntityById(mc, entityId);
|
||||
if (active == null) {
|
||||
traderGlowTicksRemaining = 0;
|
||||
traderGlowEntityId = -1;
|
||||
return;
|
||||
}
|
||||
traderGlowEntityId = entityId;
|
||||
traderGlowTicksRemaining = TRADER_HIGHLIGHT_TICKS;
|
||||
}
|
||||
|
||||
void tickContainerHighlights(Minecraft mc) {
|
||||
if (mc.level == null) return;
|
||||
if (inputContainerHighlightTicks > 0) inputContainerHighlightTicks--;
|
||||
if (outputContainerHighlightTicks > 0) outputContainerHighlightTicks--;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
final class AutoTradeVoidDelay {
|
||||
|
||||
static boolean handle(Minecraft mc, AutoTradeTickState state) {
|
||||
if (state.voidDelay <= 0) return false;
|
||||
|
||||
if (Configs.Generic.VOID_TRADING_DELAY_AFTER_TELEPORT.getBooleanValue()) {
|
||||
if (!entityStillExists(mc, state.getVillagerActive())) {
|
||||
state.voidDelay--;
|
||||
}
|
||||
} else {
|
||||
state.voidDelay--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean entityStillExists(Minecraft mc, int entityId) {
|
||||
if (mc.level == null) return false;
|
||||
for (Entity entity : mc.level.entitiesForRendering()) {
|
||||
if (entity.getId() == entityId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import com.github.sebseb7.autotrade.mixin.MultiPlayerGameModeInvoker;
|
||||
import com.github.sebseb7.autotrade.util.TradeItemSpec;
|
||||
import fi.dy.masa.malilib.gui.Message;
|
||||
import fi.dy.masa.malilib.util.InfoUtils;
|
||||
@@ -8,15 +9,18 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
//? if mc26 {
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
//?} else {
|
||||
import net.minecraft.world.inventory.ClickType;
|
||||
//?}
|
||||
import net.minecraft.world.inventory.MerchantMenu;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
final class ContainerIoHelper {
|
||||
private ContainerIoHelper() {
|
||||
@@ -24,6 +28,11 @@ final class ContainerIoHelper {
|
||||
|
||||
static void quickMoveResultSlot(Minecraft mc, AbstractContainerMenu menu, int slotIndex) {
|
||||
Slot slot = menu.getSlot(slotIndex);
|
||||
ItemStack stack = slot.getItem();
|
||||
if (menu instanceof MerchantMenu && stack.is(Items.ENCHANTED_BOOK)) {
|
||||
moveMerchantResultWithPickup(mc, menu, slot);
|
||||
return;
|
||||
}
|
||||
//? if mc26 {
|
||||
mc.gameMode.handleContainerInput(menu.containerId, slot.index, 0, ContainerInput.QUICK_MOVE, mc.player);
|
||||
//?} else {
|
||||
@@ -32,18 +41,68 @@ final class ContainerIoHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* After automated merchant packets (select trade + shift-clicks), client-side
|
||||
* prediction can drift from the server. Flush the carried/cursor stack and run
|
||||
* again on the next tick so pending slot updates have landed.
|
||||
* For {@link MerchantMenu} result stacks that are {@linkplain Items#ENCHANTED_BOOK enchanted books} only:
|
||||
* vanilla shift-click can chain every book row; use two {@code PICKUP} clicks instead.
|
||||
* Other merchant results (e.g. emeralds) still use {@code QUICK_MOVE}.
|
||||
*/
|
||||
private static void moveMerchantResultWithPickup(Minecraft mc, AbstractContainerMenu menu, Slot resultSlot) {
|
||||
if (mc.player == null || mc.gameMode == null) {
|
||||
return;
|
||||
}
|
||||
if (resultSlot.getItem().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
((MultiPlayerGameModeInvoker) mc.gameMode).invokeEnsureHasSentCarriedItem();
|
||||
containerPickupClick(mc, menu, resultSlot.index, 0);
|
||||
ItemStack carried = menu.getCarried();
|
||||
if (carried.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int depositSlotId = findMerchantDepositSlot(menu, mc.player, carried);
|
||||
if (depositSlotId < 0) {
|
||||
return;
|
||||
}
|
||||
containerPickupClick(mc, menu, depositSlotId, 0);
|
||||
}
|
||||
|
||||
private static void containerPickupClick(Minecraft mc, AbstractContainerMenu menu, int slotId, int button) {
|
||||
//? if mc26 {
|
||||
mc.gameMode.handleContainerInput(menu.containerId, slotId, button, ContainerInput.PICKUP, mc.player);
|
||||
//?} else {
|
||||
mc.gameMode.handleInventoryMouseClick(menu.containerId, slotId, button, ClickType.PICKUP, mc.player);
|
||||
//?}
|
||||
}
|
||||
|
||||
/** First player inventory slot in this menu that can accept {@code carried} (merge or empty). */
|
||||
private static int findMerchantDepositSlot(AbstractContainerMenu menu, Player player, ItemStack carried) {
|
||||
for (int i = 0; i < menu.slots.size(); i++) {
|
||||
Slot s = menu.getSlot(i);
|
||||
if (s.container != player.getInventory()) {
|
||||
continue;
|
||||
}
|
||||
ItemStack inSlot = s.getItem();
|
||||
if (inSlot.isEmpty()) {
|
||||
return s.index;
|
||||
}
|
||||
if (ItemStack.isSameItemSameComponents(inSlot, carried) && inSlot.getCount() < s.getMaxStackSize()) {
|
||||
return s.index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* After automated merchant packets, flush cursor/carried prediction so it matches the server.
|
||||
* Implemented via {@link MultiPlayerGameModeInvoker} because {@code ensureHasSentCarriedItem()} is private.
|
||||
*/
|
||||
static void syncPlayerInventoryAfterMerchant(Minecraft mc) {
|
||||
if (mc.player == null || mc.gameMode == null) {
|
||||
return;
|
||||
}
|
||||
mc.gameMode.ensureHasSentCarriedItem();
|
||||
((MultiPlayerGameModeInvoker) mc.gameMode).invokeEnsureHasSentCarriedItem();
|
||||
mc.execute(() -> {
|
||||
if (mc.player != null && mc.gameMode != null) {
|
||||
mc.gameMode.ensureHasSentCarriedItem();
|
||||
((MultiPlayerGameModeInvoker) mc.gameMode).invokeEnsureHasSentCarriedItem();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.trading.MerchantOffer;
|
||||
|
||||
/**
|
||||
* Merchant trade overlay strings and cost checks (extracted from the client tick flow).
|
||||
*/
|
||||
final class TradeFormatHelper {
|
||||
|
||||
private TradeFormatHelper() {
|
||||
}
|
||||
|
||||
static boolean playerHasMerchantCosts(Player player, MerchantOffer offer) {
|
||||
return costRequirementMet(player.getInventory(), offer.getCostA())
|
||||
&& costRequirementMet(player.getInventory(), offer.getCostB());
|
||||
}
|
||||
|
||||
private static boolean costRequirementMet(Inventory inv, ItemStack required) {
|
||||
if (required.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
int need = required.getCount();
|
||||
int have = 0;
|
||||
for (int s = 0; s < inv.getContainerSize(); s++) {
|
||||
ItemStack st = inv.getItem(s);
|
||||
if (ItemStack.isSameItemSameComponents(st, required)) {
|
||||
have += st.getCount();
|
||||
if (have >= need) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Malilib's {@code showGuiOrInGameMessage} routes to multiple HUD targets; trade spam looked like 3× duplication.
|
||||
* Vanilla overlay is a single on-screen line (same idea as vanilla toast-adjacent hints).
|
||||
*/
|
||||
static void showTradeNotice(Minecraft mc, String translationKey, Component arg1, Component arg2) {
|
||||
if (mc.gui == null) {
|
||||
return;
|
||||
}
|
||||
mc.gui.setOverlayMessage(Component.translatable(translationKey, arg1, arg2), false);
|
||||
}
|
||||
|
||||
/** e.g. "3× Book" (one villager use). */
|
||||
private static String formatItemCountAndName(ItemStack stack) {
|
||||
return stack.getCount() + "× " + stack.getHoverName().getString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-trade count × how many of this offer remain before the trade, e.g. 1
|
||||
* iron/trade × 12 runs → "12× …".
|
||||
*/
|
||||
static String formatItemCountNameForTrades(ItemStack perTrade, int remainingOfferUses) {
|
||||
if (remainingOfferUses <= 0) {
|
||||
return formatItemCountAndName(perTrade);
|
||||
}
|
||||
return (perTrade.getCount() * remainingOfferUses) + "× " + perTrade.getHoverName().getString();
|
||||
}
|
||||
|
||||
/** For buying: the stacks you pay, scaled to how many of this offer remain. */
|
||||
static String formatOfferPriceForTrades(MerchantOffer offer, int t) {
|
||||
if (t <= 0) {
|
||||
String a = offer.getCostA().isEmpty() ? null : formatItemCountAndName(offer.getCostA());
|
||||
if (offer.getCostB().isEmpty()) {
|
||||
return a != null ? a : "—";
|
||||
}
|
||||
String b = formatItemCountAndName(offer.getCostB());
|
||||
return a == null ? b : a + " + " + b;
|
||||
}
|
||||
String a = offer.getCostA().isEmpty()
|
||||
? null
|
||||
: (offer.getCostA().getCount() * t) + "× " + offer.getCostA().getHoverName().getString();
|
||||
if (offer.getCostB().isEmpty()) {
|
||||
return a != null ? a : "—";
|
||||
}
|
||||
String b = (offer.getCostB().getCount() * t) + "× " + offer.getCostB().getHoverName().getString();
|
||||
return a == null ? b : a + " + " + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the trade has a second cost item, " + 2× …" scaled to remaining offer
|
||||
* uses.
|
||||
*/
|
||||
static String formatOptionalSecondCostForTrades(MerchantOffer offer, int t) {
|
||||
if (offer.getCostB().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (t <= 0) {
|
||||
return " + " + formatItemCountAndName(offer.getCostB());
|
||||
}
|
||||
return " + " + (offer.getCostB().getCount() * t) + "× " + offer.getCostB().getHoverName().getString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.github.sebseb7.autotrade.event;
|
||||
|
||||
import com.github.sebseb7.autotrade.AutoTrade;
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import java.util.Vector;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
//? if npcSplit {
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
|
||||
//?}
|
||||
//? if npcFlat {
|
||||
import net.minecraft.world.entity.npc.Villager;
|
||||
import net.minecraft.world.entity.npc.WanderingTrader;
|
||||
//?}
|
||||
import net.minecraft.world.level.ClipContext;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Finds nearby villagers/wandering traders and performs the synthetic interact used to open trades.
|
||||
*/
|
||||
final class TraderInteractor {
|
||||
private static final int LOS_TIMEOUT_TICKS = 60;
|
||||
|
||||
private final AutoTradeTickState state;
|
||||
private final Vector<Entity> villagersInRange = new Vector<>();
|
||||
private int rotatingTargetId = -1;
|
||||
private int rotatingTargetTicks = 0;
|
||||
|
||||
TraderInteractor(AutoTradeTickState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
int getVillagerActive() {
|
||||
return state.villagerActive;
|
||||
}
|
||||
|
||||
boolean findAndInteract(Minecraft mc) {
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return false;
|
||||
}
|
||||
boolean found = false;
|
||||
Vector<Entity> newVillagersInRange = new Vector<>(villagersInRange);
|
||||
for (Entity entity : mc.level.entitiesForRendering()) {
|
||||
if (entity instanceof Villager || entity instanceof WanderingTrader) {
|
||||
if (entity.distanceToSqr(mc.player) < (2.5f * 2.5f)) {
|
||||
if (!found) {
|
||||
if (!newVillagersInRange.contains(entity)) {
|
||||
found = true;
|
||||
Vec3 aimPoint = firstVisiblePoint(mc, entity);
|
||||
if (rotatingTargetId != entity.getId()) {
|
||||
rotatingTargetId = entity.getId();
|
||||
rotatingTargetTicks = 0;
|
||||
Vec3 lookAt = aimPoint != null ? aimPoint : entity.getEyePosition();
|
||||
Vec3 eyePos = mc.player.getEyePosition();
|
||||
Vec3 delta = lookAt.subtract(eyePos);
|
||||
double horiz = Math.sqrt(delta.x * delta.x + delta.z * delta.z);
|
||||
float yaw = (float) (Math.toDegrees(Math.atan2(delta.z, delta.x)) - 90.0);
|
||||
float pitch = (float) -Math.toDegrees(Math.atan2(delta.y, horiz));
|
||||
mc.player.setYRot(yaw);
|
||||
mc.player.setXRot(pitch);
|
||||
mc.player.yHeadRot = yaw;
|
||||
} else {
|
||||
rotatingTargetTicks++;
|
||||
}
|
||||
if (aimPoint == null) {
|
||||
if (rotatingTargetTicks > LOS_TIMEOUT_TICKS) {
|
||||
newVillagersInRange.add(entity);
|
||||
rotatingTargetId = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
newVillagersInRange.add(entity);
|
||||
rotatingTargetId = -1;
|
||||
EntityHitResult ehr = new EntityHitResult(entity, aimPoint);
|
||||
AutoTrade.autoInteracting = true;
|
||||
try {
|
||||
//? if mc26 {
|
||||
mc.gameMode.interact(mc.player, entity, ehr, InteractionHand.MAIN_HAND);
|
||||
//?} else {
|
||||
mc.gameMode.interactAt(mc.player, entity, ehr, InteractionHand.MAIN_HAND);
|
||||
mc.gameMode.interact(mc.player, entity, InteractionHand.MAIN_HAND);
|
||||
//?}
|
||||
} finally {
|
||||
AutoTrade.autoInteracting = false;
|
||||
}
|
||||
mc.player.swing(InteractionHand.MAIN_HAND);
|
||||
state.postMerchantInventorySyncTicks = 0;
|
||||
state.voidDelay = Configs.Generic.VOID_TRADING_DELAY.getIntegerValue();
|
||||
state.villagerActive = entity.getId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Entity entity : villagersInRange) {
|
||||
if (entity.distanceToSqr(mc.player) >= 16.0D) {
|
||||
newVillagersInRange.remove(entity);
|
||||
}
|
||||
}
|
||||
villagersInRange.clear();
|
||||
villagersInRange.addAll(newVillagersInRange);
|
||||
return found;
|
||||
}
|
||||
|
||||
static Entity findEntityById(Minecraft mc, int entityId) {
|
||||
if (mc.level == null) {
|
||||
return null;
|
||||
}
|
||||
for (Entity e : mc.level.entitiesForRendering()) {
|
||||
if (e.getId() == entityId) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first point on {@code target}'s bounding box (eyes, head,
|
||||
* chest, feet, or one of the four upper corners) that has an unobstructed
|
||||
* block ray-cast from the player's eyes; {@code null} when the entire body
|
||||
* is occluded.
|
||||
*/
|
||||
private static Vec3 firstVisiblePoint(Minecraft mc, Entity target) {
|
||||
Vec3 eye = mc.player.getEyePosition();
|
||||
AABB box = target.getBoundingBox();
|
||||
double cx = (box.minX + box.maxX) * 0.5;
|
||||
double cz = (box.minZ + box.maxZ) * 0.5;
|
||||
Vec3[] candidates = {
|
||||
target.getEyePosition(),
|
||||
new Vec3(cx, (box.minY + box.maxY) * 0.5, cz),
|
||||
new Vec3(cx, box.maxY - 0.05, cz),
|
||||
new Vec3(cx, box.minY + 0.1, cz),
|
||||
new Vec3(box.minX + 0.05, box.maxY - 0.2, box.minZ + 0.05),
|
||||
new Vec3(box.maxX - 0.05, box.maxY - 0.2, box.minZ + 0.05),
|
||||
new Vec3(box.minX + 0.05, box.maxY - 0.2, box.maxZ - 0.05),
|
||||
new Vec3(box.maxX - 0.05, box.maxY - 0.2, box.maxZ - 0.05),
|
||||
};
|
||||
for (Vec3 p : candidates) {
|
||||
BlockHitResult hit = mc.level.clip(new ClipContext(eye, p, ClipContext.Block.COLLIDER,
|
||||
ClipContext.Fluid.NONE, mc.player));
|
||||
if (hit.getType() == HitResult.Type.MISS
|
||||
|| eye.distanceToSqr(hit.getLocation()) >= eye.distanceToSqr(p) - 1.0E-4) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,27 @@
|
||||
package com.github.sebseb7.autotrade.gui;
|
||||
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import com.github.sebseb7.autotrade.mixin.MerchantMenuAccessor;
|
||||
import com.github.sebseb7.autotrade.render.VillagerTradeCache;
|
||||
import com.github.sebseb7.autotrade.util.TradeItemSpec;
|
||||
import fi.dy.masa.malilib.gui.GuiBase;
|
||||
import fi.dy.masa.malilib.gui.Message;
|
||||
import fi.dy.masa.malilib.util.InfoUtils;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
|
||||
import net.fabricmc.fabric.api.client.screen.v1.Screens;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.StringWidget;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.ContainerScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.ShulkerBoxScreen;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
//? if npcSplit {
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
|
||||
@@ -18,6 +30,10 @@ import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
|
||||
import net.minecraft.world.entity.npc.Villager;
|
||||
import net.minecraft.world.entity.npc.WanderingTrader;
|
||||
//?}
|
||||
import net.minecraft.world.inventory.MerchantMenu;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.trading.MerchantOffer;
|
||||
import net.minecraft.world.item.trading.MerchantOffers;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
@@ -30,42 +46,327 @@ public final class MerchantScreenButtonInjector {
|
||||
ScreenEvents.AFTER_INIT.register(MerchantScreenButtonInjector::onScreenInit);
|
||||
}
|
||||
|
||||
private static void onScreenInit(Minecraft client, Screen screen, int scaledWidth, int scaledHeight) {
|
||||
if (!(screen instanceof MerchantScreen merchantScreen)) {
|
||||
return;
|
||||
/**
|
||||
* Try to get the block position from a screen's menu.
|
||||
* Supports MerchantScreen (villager/trader), ShulkerBoxScreen, and ContainerScreen (chests).
|
||||
*/
|
||||
private static BlockPos getContainerPos(Screen screen) {
|
||||
if (screen instanceof MerchantScreen) {
|
||||
// For merchant screens, we don't have a block position in the same way
|
||||
// Return null - the merchant screen buttons don't need this
|
||||
return null;
|
||||
}
|
||||
// For container screens, try to get the block position from the player's last block hit result
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.hitResult != null && mc.hitResult.getType() == HitResult.Type.BLOCK) {
|
||||
BlockHitResult blockHit = (BlockHitResult) mc.hitResult;
|
||||
return blockHit.getBlockPos();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void onScreenInit(Minecraft client, Screen screen, int scaledWidth, int scaledHeight) {
|
||||
// Determine screen type and set up appropriate buttons
|
||||
if (screen instanceof MerchantScreen merchantScreen) {
|
||||
addMerchantScreenButtons(client, merchantScreen, scaledWidth, scaledHeight);
|
||||
} else if (screen instanceof ShulkerBoxScreen shulkerScreen) {
|
||||
addContainerScreenButtons(client, shulkerScreen, scaledWidth, scaledHeight, "Shulker Box");
|
||||
} else if (screen instanceof ContainerScreen) {
|
||||
// Add buttons for chest and other container screens
|
||||
addContainerScreenButtons(client, (ContainerScreen) screen, scaledWidth, scaledHeight, "Chest");
|
||||
}
|
||||
}
|
||||
|
||||
/** Add buttons to merchant (villager/trader) screens. */
|
||||
private static void addMerchantScreenButtons(Minecraft client, MerchantScreen merchantScreen, int scaledWidth, int scaledHeight) {
|
||||
// Position buttons safely to the right of the merchant GUI (276 px wide, centered).
|
||||
int x = scaledWidth / 2 + 140;
|
||||
int y = scaledHeight / 2 - 83;
|
||||
int bw = 160;
|
||||
int lw = 110;
|
||||
int h = 20;
|
||||
int gap = 2;
|
||||
int labelX = x + bw + 4;
|
||||
|
||||
Button openSettings = Button
|
||||
.builder(Component.literal("Open Settings"), btn -> {
|
||||
// vanilla Screen.onClose -> setScreen(null) sends the container-close
|
||||
// packet; switching the screen out from under the merchant GUI via
|
||||
// GuiBase.openGui skips that path, so the server still thinks we are
|
||||
// trading with this villager and rejects the next interact packet.
|
||||
if (client.player != null) {
|
||||
client.player.closeContainer();
|
||||
}
|
||||
GuiBase.openGui(new GuiConfigs());
|
||||
})
|
||||
.bounds(x, y, bw, h).build();
|
||||
|
||||
Button selectSell = Button
|
||||
.builder(sellButtonLabel(null), btn -> onSellButton(client, merchantScreen))
|
||||
.bounds(x, y + (h + gap), bw, h).build();
|
||||
StringWidget sellCurrent = new StringWidget(labelX, y + (h + gap), lw, h,
|
||||
currentSellLabel(), client.font);
|
||||
|
||||
Button selectBuy = Button
|
||||
.builder(buyButtonLabel(null), btn -> onBuyButton(client, merchantScreen))
|
||||
.bounds(x, y + 2 * (h + gap), bw, h).build();
|
||||
StringWidget buyCurrent = new StringWidget(labelX, y + 2 * (h + gap), lw, h,
|
||||
currentBuyLabel(), client.font);
|
||||
|
||||
Button enableAutotrade = Button
|
||||
.builder(autotradeButtonLabel(), btn -> toggleAutotrade(btn))
|
||||
.bounds(x, y + 3 * (h + gap), bw, h).build();
|
||||
|
||||
// Add button during AFTER_INIT so it is properly registered as renderable.
|
||||
// We position it safely to the right of the merchant GUI.
|
||||
// The merchant GUI is 276 pixels wide and centered.
|
||||
Button button = Button
|
||||
.builder(Component.literal("Select Enchantments"),
|
||||
btn -> client.setScreen(new EnchantmentSelectionScreen(merchantScreen)))
|
||||
.bounds(scaledWidth / 2 + 140, scaledHeight / 2 - 83, 120, 20).build();
|
||||
Screen asScreen = merchantScreen;
|
||||
//? if mc26 {
|
||||
Screens.getWidgets(asScreen).add(button);
|
||||
Screens.getWidgets(asScreen).add(openSettings);
|
||||
Screens.getWidgets(asScreen).add(selectSell);
|
||||
Screens.getWidgets(asScreen).add(sellCurrent);
|
||||
Screens.getWidgets(asScreen).add(selectBuy);
|
||||
Screens.getWidgets(asScreen).add(buyCurrent);
|
||||
Screens.getWidgets(asScreen).add(enableAutotrade);
|
||||
//?} else {
|
||||
Screens.getButtons(asScreen).add(button);
|
||||
Screens.getButtons(asScreen).add(openSettings);
|
||||
Screens.getButtons(asScreen).add(selectSell);
|
||||
Screens.getButtons(asScreen).add(sellCurrent);
|
||||
Screens.getButtons(asScreen).add(selectBuy);
|
||||
Screens.getButtons(asScreen).add(buyCurrent);
|
||||
Screens.getButtons(asScreen).add(enableAutotrade);
|
||||
//?}
|
||||
|
||||
// Offers arrive via a server packet after the screen opens.
|
||||
// Register a per-screen tick handler to wait for offers and cache them.
|
||||
final boolean[] handled = {false};
|
||||
// Register a per-screen tick handler to wait for offers, cache them,
|
||||
// and refresh dynamic button state (active/label).
|
||||
final boolean[] cached = {false};
|
||||
ScreenEvents.afterTick(merchantScreen).register(s -> {
|
||||
if (handled[0]) {
|
||||
return;
|
||||
}
|
||||
MerchantOffers offers = merchantScreen.getMenu().getOffers();
|
||||
if (offers == null || offers.isEmpty()) {
|
||||
return; // not yet synced
|
||||
if (!cached[0] && offers != null && !offers.isEmpty()) {
|
||||
cached[0] = true;
|
||||
cacheOffersForNearestTrader(client, offers);
|
||||
}
|
||||
handled[0] = true;
|
||||
|
||||
cacheOffersForNearestTrader(client, offers);
|
||||
MerchantOffer current = currentSelectedOffer(merchantScreen);
|
||||
MerchantOffer sellOffer = (current != null && isSellOffer(current)) ? current : null;
|
||||
MerchantOffer buyOffer = (current != null && isBuyOffer(current)) ? current : null;
|
||||
boolean sellOn = Configs.Generic.ENABLE_SELL.getBooleanValue();
|
||||
boolean buyOn = Configs.Generic.ENABLE_BUY.getBooleanValue();
|
||||
// Button doubles as a "Disable" toggle when no applicable trade is selected
|
||||
// but the corresponding ENABLE flag is currently on — so the user can turn
|
||||
// it off without leaving the merchant screen.
|
||||
selectSell.active = sellOffer != null || sellOn;
|
||||
selectBuy.active = buyOffer != null || buyOn;
|
||||
selectSell.setMessage(sellOffer != null ? sellButtonLabel(sellOffer)
|
||||
: (sellOn ? Component.literal("Disable sell") : sellButtonLabel(null)));
|
||||
selectBuy.setMessage(buyOffer != null ? buyButtonLabel(buyOffer)
|
||||
: (buyOn ? Component.literal("Disable buy") : buyButtonLabel(null)));
|
||||
sellCurrent.setMessage(currentSellLabel());
|
||||
buyCurrent.setMessage(currentBuyLabel());
|
||||
enableAutotrade.setMessage(autotradeButtonLabel());
|
||||
});
|
||||
}
|
||||
|
||||
/** Add buttons to container screens (shulker boxes and chests). */
|
||||
private static void addContainerScreenButtons(Minecraft client, Screen screen, int scaledWidth, int scaledHeight, String containerName) {
|
||||
// Position buttons to the right of the container GUI
|
||||
int x = scaledWidth / 2 + 140;
|
||||
int y = scaledHeight / 2 - 83;
|
||||
int bw = 160;
|
||||
int h = 20;
|
||||
int gap = 2;
|
||||
|
||||
Button openSettings = Button
|
||||
.builder(Component.literal("Open Settings"), btn -> {
|
||||
if (client.player != null) {
|
||||
client.player.closeContainer();
|
||||
}
|
||||
GuiBase.openGui(new GuiConfigs());
|
||||
})
|
||||
.bounds(x, y, bw, h).build();
|
||||
|
||||
Button setAsInput = Button
|
||||
.builder(Component.literal("Set as Autotrade Input"), btn -> {
|
||||
BlockPos pos = getContainerPos(screen);
|
||||
if (pos != null) {
|
||||
Configs.Generic.INPUT_CONTAINER_X.setIntegerValue(pos.getX());
|
||||
Configs.Generic.INPUT_CONTAINER_Y.setIntegerValue(pos.getY());
|
||||
Configs.Generic.INPUT_CONTAINER_Z.setIntegerValue(pos.getZ());
|
||||
Configs.saveToFile();
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO,
|
||||
"autotrade.message.input_container_set", containerName, pos.toShortString());
|
||||
} else {
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.ERROR,
|
||||
"autotrade.message.container_pos_error");
|
||||
}
|
||||
})
|
||||
.bounds(x, y + (h + gap), bw, h).build();
|
||||
|
||||
Button setAsOutput = Button
|
||||
.builder(Component.literal("Set as Autotrade Output"), btn -> {
|
||||
BlockPos pos = getContainerPos(screen);
|
||||
if (pos != null) {
|
||||
Configs.Generic.OUTPUT_CONTAINER_X.setIntegerValue(pos.getX());
|
||||
Configs.Generic.OUTPUT_CONTAINER_Y.setIntegerValue(pos.getY());
|
||||
Configs.Generic.OUTPUT_CONTAINER_Z.setIntegerValue(pos.getZ());
|
||||
Configs.saveToFile();
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO,
|
||||
"autotrade.message.output_container_set", containerName, pos.toShortString());
|
||||
} else {
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.ERROR,
|
||||
"autotrade.message.container_pos_error");
|
||||
}
|
||||
})
|
||||
.bounds(x, y + 2 * (h + gap), bw, h).build();
|
||||
|
||||
//? if mc26 {
|
||||
Screens.getWidgets(screen).add(openSettings);
|
||||
Screens.getWidgets(screen).add(setAsInput);
|
||||
Screens.getWidgets(screen).add(setAsOutput);
|
||||
//?} else {
|
||||
Screens.getButtons(screen).add(openSettings);
|
||||
Screens.getButtons(screen).add(setAsInput);
|
||||
Screens.getButtons(screen).add(setAsOutput);
|
||||
//?}
|
||||
}
|
||||
|
||||
private static void onSellButton(Minecraft client, MerchantScreen screen) {
|
||||
MerchantOffer offer = currentSelectedOffer(screen);
|
||||
if (offer != null && isSellOffer(offer)) {
|
||||
applySelectedTradeAsSell(client, screen);
|
||||
return;
|
||||
}
|
||||
if (Configs.Generic.ENABLE_SELL.getBooleanValue()) {
|
||||
Configs.Generic.ENABLE_SELL.setBooleanValue(false);
|
||||
Configs.saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
private static void onBuyButton(Minecraft client, MerchantScreen screen) {
|
||||
MerchantOffer offer = currentSelectedOffer(screen);
|
||||
if (offer != null && isBuyOffer(offer)) {
|
||||
applySelectedTradeAsBuy(client, screen);
|
||||
return;
|
||||
}
|
||||
if (Configs.Generic.ENABLE_BUY.getBooleanValue()) {
|
||||
Configs.Generic.ENABLE_BUY.setBooleanValue(false);
|
||||
Configs.saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
private static Component autotradeButtonLabel() {
|
||||
boolean on = Configs.Generic.ENABLED.getBooleanValue();
|
||||
return Component.literal(on ? "Disable Autotrade" : "Enable Autotrade");
|
||||
}
|
||||
|
||||
/** Button label previews the item that would be written if clicked. */
|
||||
private static Component sellButtonLabel(MerchantOffer offer) {
|
||||
String item = offer == null ? "-" : describeSpec(TradeItemSpec.encodeFromStack(offer.getCostA()));
|
||||
return Component.literal("Set " + item + " as sell item");
|
||||
}
|
||||
|
||||
private static Component buyButtonLabel(MerchantOffer offer) {
|
||||
String item = offer == null ? "-" : describeSpec(TradeItemSpec.encodeFromStack(offer.getResult()));
|
||||
return Component.literal("Set " + item + " as buy item");
|
||||
}
|
||||
|
||||
/** Side-label shows the currently-configured sell/buy item from config. */
|
||||
private static Component currentSellLabel() {
|
||||
String off = Configs.Generic.ENABLE_SELL.getBooleanValue() ? "" : " (off)";
|
||||
return Component.literal(describeSpec(Configs.Generic.SELL_ITEM.getStringValue()) + off);
|
||||
}
|
||||
|
||||
private static Component currentBuyLabel() {
|
||||
String off = Configs.Generic.ENABLE_BUY.getBooleanValue() ? "" : " (off)";
|
||||
return Component.literal(describeSpec(Configs.Generic.BUY_ITEM.getStringValue()) + off);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact human-readable form of a {@link com.github.sebseb7.autotrade.util.TradeItemSpec}
|
||||
* string. Strips the {@code minecraft:} namespace and renders enchant
|
||||
* suffixes after a {@code +}, e.g. {@code minecraft:enchanted_book#minecraft:mending=1}
|
||||
* → {@code enchanted_book +mending=1}.
|
||||
*/
|
||||
private static String describeSpec(String spec) {
|
||||
if (spec == null || spec.isEmpty()) {
|
||||
return "(none)";
|
||||
}
|
||||
int sep = spec.indexOf('#');
|
||||
String itemPart = sep < 0 ? spec : spec.substring(0, sep);
|
||||
String name = stripVanillaNs(itemPart);
|
||||
if (sep < 0) {
|
||||
return name;
|
||||
}
|
||||
String enchants = spec.substring(sep + 1).replace("minecraft:", "");
|
||||
return name + " +" + enchants;
|
||||
}
|
||||
|
||||
private static String stripVanillaNs(String id) {
|
||||
return id.startsWith("minecraft:") ? id.substring("minecraft:".length()) : id;
|
||||
}
|
||||
|
||||
private static void toggleAutotrade(Button btn) {
|
||||
Configs.Generic.ENABLED.toggleBooleanValue();
|
||||
boolean enabled = Configs.Generic.ENABLED.getBooleanValue();
|
||||
btn.setMessage(autotradeButtonLabel());
|
||||
String msg = enabled ? "autotrade.message.toggled_mod_on" : "autotrade.message.toggled_mod_off";
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, msg);
|
||||
Configs.saveToFile();
|
||||
}
|
||||
|
||||
private static MerchantOffer currentSelectedOffer(MerchantScreen screen) {
|
||||
MerchantMenu menu = screen.getMenu();
|
||||
MerchantOffers offers = menu.getOffers();
|
||||
if (offers == null || offers.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int idx = ((MerchantMenuAccessor) (Object) screen).getShopItem();
|
||||
if (idx < 0 || idx >= offers.size()) {
|
||||
return null;
|
||||
}
|
||||
return offers.get(idx);
|
||||
}
|
||||
|
||||
/** A "sell" trade pays the player in emeralds for an item. */
|
||||
private static boolean isSellOffer(MerchantOffer offer) {
|
||||
return offer.getResult().is(Items.EMERALD) && !offer.getCostA().isEmpty()
|
||||
&& !offer.getCostA().is(Items.EMERALD);
|
||||
}
|
||||
|
||||
/** A "buy" trade gives the player an item in exchange for emeralds. */
|
||||
private static boolean isBuyOffer(MerchantOffer offer) {
|
||||
if (offer.getResult().isEmpty() || offer.getResult().is(Items.EMERALD)) {
|
||||
return false;
|
||||
}
|
||||
return offer.getCostA().is(Items.EMERALD) || offer.getCostB().is(Items.EMERALD);
|
||||
}
|
||||
|
||||
private static void applySelectedTradeAsSell(Minecraft client, MerchantScreen screen) {
|
||||
MerchantOffer offer = currentSelectedOffer(screen);
|
||||
if (offer == null || !isSellOffer(offer)) {
|
||||
return;
|
||||
}
|
||||
ItemStack item = offer.getCostA();
|
||||
String spec = TradeItemSpec.encodeFromStack(item);
|
||||
Configs.Generic.SELL_ITEM.setValueFromString(spec);
|
||||
Configs.Generic.SELL_LIMIT.setIntegerValue(item.getCount());
|
||||
Configs.Generic.ENABLE_SELL.setBooleanValue(true);
|
||||
Configs.saveToFile();
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set", spec);
|
||||
}
|
||||
|
||||
private static void applySelectedTradeAsBuy(Minecraft client, MerchantScreen screen) {
|
||||
MerchantOffer offer = currentSelectedOffer(screen);
|
||||
if (offer == null || !isBuyOffer(offer)) {
|
||||
return;
|
||||
}
|
||||
ItemStack item = offer.getResult();
|
||||
String spec = TradeItemSpec.encodeFromStack(item);
|
||||
Configs.Generic.BUY_ITEM.setValueFromString(spec);
|
||||
Configs.Generic.BUY_LIMIT.setIntegerValue(item.getCount());
|
||||
Configs.Generic.ENABLE_BUY.setBooleanValue(true);
|
||||
Configs.saveToFile();
|
||||
InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set", spec);
|
||||
}
|
||||
|
||||
private static void cacheOffersForNearestTrader(Minecraft mc, MerchantOffers offers) {
|
||||
if (mc.player == null || mc.level == null) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.github.sebseb7.autotrade.mixin;
|
||||
|
||||
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
/**
|
||||
* Exposes the currently-selected trade index from {@link MerchantScreen}.
|
||||
*
|
||||
* <p>{@code MerchantMenu#selectionHint} is server-side hint state and is not
|
||||
* persisted on the client in newer Minecraft versions, so the only reliable
|
||||
* client source of truth is the screen's {@code shopItem} field which is
|
||||
* updated by the trade buttons.
|
||||
*/
|
||||
@Mixin(MerchantScreen.class)
|
||||
public interface MerchantMenuAccessor {
|
||||
|
||||
@Accessor("shopItem")
|
||||
int getShopItem();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.github.sebseb7.autotrade.mixin;
|
||||
|
||||
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
@Mixin(MultiPlayerGameMode.class)
|
||||
public interface MultiPlayerGameModeInvoker {
|
||||
|
||||
@Invoker("ensureHasSentCarriedItem")
|
||||
void invokeEnsureHasSentCarriedItem();
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
package com.github.sebseb7.autotrade.render;
|
||||
|
||||
//? if mc26 {
|
||||
//? if traderWireframeRender {
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import com.github.sebseb7.autotrade.event.KeybindCallbacks;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.ShapeRenderer;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
@@ -18,6 +15,20 @@ import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
//?}
|
||||
//? if traderWireframeMc26 {
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
//?}
|
||||
//? if traderWireframe121 {
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderEvents;
|
||||
//?}
|
||||
//? if traderWireframe12110 {
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
//?}
|
||||
//? if traderWireframeMc26 || traderWireframe12111 {
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
//?}
|
||||
|
||||
/**
|
||||
* Client wireframe highlights: last-traded villager, and input/output container
|
||||
@@ -30,21 +41,27 @@ public final class TraderHighlightRenderer {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
//? if mc26 {
|
||||
LevelRenderEvents.AFTER_SOLID_FEATURES.register(TraderHighlightRenderer::renderLevel);
|
||||
//? if traderWireframeMc26 {
|
||||
LevelRenderEvents.AFTER_SOLID_FEATURES.register(TraderHighlightRenderer::renderLevelMc26);
|
||||
//?}
|
||||
//? if traderWireframe121 {
|
||||
WorldRenderEvents.END_MAIN.register(TraderHighlightRenderer::renderWorld121);
|
||||
//?}
|
||||
}
|
||||
|
||||
//? if mc26 {
|
||||
//? if traderWireframeRender {
|
||||
private static final ShapeRenderer SHAPE_RENDERER = new ShapeRenderer();
|
||||
|
||||
private static final int TRADER_OUTLINE_COLOR = 0xFF66FF66;
|
||||
private static final int INPUT_CONTAINER_COLOR = 0xFFFF6666;
|
||||
private static final int OUTPUT_CONTAINER_COLOR = 0xFF6666FF;
|
||||
|
||||
//? if traderWireframeMc26 || traderWireframe12111 {
|
||||
private static final float LINE_WIDTH = 2.5F;
|
||||
//?}
|
||||
|
||||
private static void renderLevel(LevelRenderContext context) {
|
||||
//? if traderWireframeMc26 {
|
||||
private static void renderLevelMc26(LevelRenderContext context) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.level == null) {
|
||||
return;
|
||||
@@ -65,6 +82,38 @@ public final class TraderHighlightRenderer {
|
||||
|
||||
renderBoxes(mc, drawPose, consumer, camera, tickDelta, trader, inTicks, outTicks);
|
||||
}
|
||||
//?}
|
||||
|
||||
//? if traderWireframe121 {
|
||||
private static void renderWorld121(WorldRenderContext context) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.level == null) {
|
||||
return;
|
||||
}
|
||||
KeybindCallbacks kb = KeybindCallbacks.getInstance();
|
||||
Entity trader = kb.getTraderHighlightEntity(mc);
|
||||
int inTicks = kb.getInputContainerHighlightTicks();
|
||||
int outTicks = kb.getOutputContainerHighlightTicks();
|
||||
if (trader == null && inTicks <= 0 && outTicks <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var vertexConsumers = context.consumers();
|
||||
PoseStack drawPose = new PoseStack();
|
||||
Vec3 camera = mc.gameRenderer.getMainCamera().position();
|
||||
float tickDelta = mc.getDeltaTracker().getGameTimeDeltaPartialTick(true);
|
||||
|
||||
final VertexConsumer consumer;
|
||||
//? if traderWireframe12110 {
|
||||
consumer = vertexConsumers.getBuffer(RenderType.lines());
|
||||
//?}
|
||||
//? if traderWireframe12111 {
|
||||
consumer = vertexConsumers.getBuffer(RenderTypes.lines());
|
||||
//?}
|
||||
|
||||
renderBoxes(mc, drawPose, consumer, camera, tickDelta, trader, inTicks, outTicks);
|
||||
}
|
||||
//?}
|
||||
|
||||
private static void renderBoxes(Minecraft mc, PoseStack drawPose, VertexConsumer consumer, Vec3 camera,
|
||||
float tickDelta, Entity trader, int inTicks, int outTicks) {
|
||||
@@ -100,8 +149,13 @@ public final class TraderHighlightRenderer {
|
||||
}
|
||||
|
||||
private static void renderShape(PoseStack drawPose, VertexConsumer consumer, AABB cameraRelative, int color) {
|
||||
//? if traderWireframe12110 {
|
||||
SHAPE_RENDERER.renderShape(drawPose, consumer, Shapes.create(cameraRelative), 0.0D, 0.0D, 0.0D, color);
|
||||
//?}
|
||||
//? if traderWireframeMc26 || traderWireframe12111 {
|
||||
SHAPE_RENDERER.renderShape(drawPose, consumer, Shapes.create(cameraRelative), 0.0D, 0.0D, 0.0D, color,
|
||||
LINE_WIDTH);
|
||||
//?}
|
||||
}
|
||||
//?}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ import net.minecraft.world.item.trading.MerchantOffers;
|
||||
*
|
||||
* <p>
|
||||
* Populated by {@code AutoTradeClientTick} when the mod opens a merchant
|
||||
* screen; consumed by {@link VillagerTradeOverlayRenderer} to draw trade labels
|
||||
* above each villager's head.
|
||||
* screen.
|
||||
*/
|
||||
public final class VillagerTradeCache {
|
||||
private static final Map<UUID, MerchantOffers> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package com.github.sebseb7.autotrade.render;
|
||||
|
||||
import com.github.sebseb7.autotrade.config.Configs;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
//? if mc26 {
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
//?}
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
//? if mc26 {
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
|
||||
//?}
|
||||
import net.minecraft.world.item.trading.MerchantOffer;
|
||||
import net.minecraft.world.item.trading.MerchantOffers;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Renders a compact summary of each villager's known trades above their head.
|
||||
*
|
||||
* <p>
|
||||
* Trade data comes from {@link VillagerTradeCache}, which is populated when the
|
||||
* mod opens a merchant screen. Villagers whose trades haven't been seen yet
|
||||
* show nothing.
|
||||
*/
|
||||
public final class VillagerTradeOverlayRenderer {
|
||||
|
||||
/** World-space scale of the text (vanilla name-tags use ~0.025). */
|
||||
private static final float TEXT_SCALE = 0.02F;
|
||||
|
||||
/** Text background colour (semi-transparent dark). */
|
||||
private static final int BG_COLOR = 0x80000000;
|
||||
|
||||
/** Normal trade text colour (white). */
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
|
||||
/** Depleted trade text colour (grey/red). */
|
||||
private static final int DEPLETED_COLOR = 0xFFFF6666;
|
||||
|
||||
private VillagerTradeOverlayRenderer() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
//? if mc26 {
|
||||
LevelRenderEvents.COLLECT_SUBMITS.register(VillagerTradeOverlayRenderer::renderLevel);
|
||||
//?}
|
||||
}
|
||||
|
||||
//? if mc26 {
|
||||
private static void renderLevel(LevelRenderContext context) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
if (!Configs.Generic.SHOW_TRADES.getBooleanValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Font font = mc.font;
|
||||
Vec3 camera = mc.gameRenderer.getMainCamera().position();
|
||||
float tickDelta = mc.getDeltaTracker().getGameTimeDeltaPartialTick(true);
|
||||
|
||||
for (Entity entity : mc.level.entitiesForRendering()) {
|
||||
if (!(entity instanceof Villager) && !(entity instanceof WanderingTrader)) {
|
||||
continue;
|
||||
}
|
||||
if (entity.distanceToSqr(mc.player) > 64.0 * 64.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MerchantOffers offers = VillagerTradeCache.get(entity.getUUID());
|
||||
if (offers == null || offers.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<TradeLineEntry> lines = buildTradeLines(offers);
|
||||
if (lines.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double x = Mth.lerp(tickDelta, entity.xOld, entity.getX()) - camera.x;
|
||||
double y = Mth.lerp(tickDelta, entity.yOld, entity.getY()) - camera.y;
|
||||
double z = Mth.lerp(tickDelta, entity.zOld, entity.getZ()) - camera.z;
|
||||
|
||||
float baseY = entity.getBbHeight() + 0.6F;
|
||||
|
||||
PoseStack poseStack = new PoseStack();
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(x, y + baseY, z);
|
||||
|
||||
poseStack.mulPose(mc.gameRenderer.getMainCamera().rotation());
|
||||
poseStack.scale(-TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE);
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
TradeLineEntry entry = lines.get(i);
|
||||
float lineOffsetY = -(lines.size() - 1 - i) * (font.lineHeight + 2);
|
||||
|
||||
Matrix4f matrix = poseStack.last().pose();
|
||||
matrix = new Matrix4f(matrix);
|
||||
matrix.translate(0, lineOffsetY, 0);
|
||||
|
||||
int textWidth = font.width(entry.text);
|
||||
float textX = -textWidth / 2.0F;
|
||||
|
||||
context.submitNodeCollector().submitText(poseStack, textX, 0,
|
||||
net.minecraft.network.chat.Component.literal(entry.text).getVisualOrderText(), false,
|
||||
Font.DisplayMode.NORMAL, entry.color, 0xF000F0, BG_COLOR, 0);
|
||||
}
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
|
||||
//?}
|
||||
|
||||
private static List<TradeLineEntry> buildTradeLines(MerchantOffers offers) {
|
||||
List<TradeLineEntry> lines = new ArrayList<>();
|
||||
for (int i = 0; i < offers.size(); i++) {
|
||||
MerchantOffer offer = offers.get(i);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (!offer.getCostA().isEmpty()) {
|
||||
sb.append(offer.getCostA().getCount()).append("× ").append(offer.getCostA().getHoverName().getString());
|
||||
}
|
||||
|
||||
if (!offer.getCostB().isEmpty()) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" + ");
|
||||
}
|
||||
sb.append(offer.getCostB().getCount()).append("× ").append(offer.getCostB().getHoverName().getString());
|
||||
}
|
||||
|
||||
sb.append(" → ");
|
||||
|
||||
sb.append(offer.getResult().getCount()).append("× ").append(offer.getResult().getHoverName().getString());
|
||||
|
||||
int remaining = offer.getMaxUses() - offer.getUses();
|
||||
sb.append(" (").append(remaining).append("/").append(offer.getMaxUses()).append(")");
|
||||
|
||||
boolean depleted = remaining <= 0;
|
||||
lines.add(new TradeLineEntry(sb.toString(), depleted ? DEPLETED_COLOR : TEXT_COLOR));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private record TradeLineEntry(String text, int color) {
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.github.sebseb7.autotrade.util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
@@ -15,7 +14,7 @@ import net.minecraft.world.item.enchantment.ItemEnchantments;
|
||||
* item. For enchanted books (and other enchanted items), holding the item when
|
||||
* binding the hotkey stores
|
||||
* {@code minecraft:enchanted_book#minecraft:sharpness=4&minecraft:unbreaking=3}
|
||||
* so only that exact enchantment set is matched.
|
||||
* so only that exact enchantment set is matched (same ids and levels as on the stack).
|
||||
*/
|
||||
public final class TradeItemSpec {
|
||||
private static final char SPEC_SEP = '#';
|
||||
@@ -23,15 +22,26 @@ public final class TradeItemSpec {
|
||||
private TradeItemSpec() {
|
||||
}
|
||||
|
||||
private static String normalizeEnchantId(String id) {
|
||||
String t = id.trim();
|
||||
if (t.isEmpty()) {
|
||||
return t;
|
||||
}
|
||||
if (t.indexOf(':') < 0) {
|
||||
return "minecraft:" + t;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
public static String encodeFromStack(ItemStack stack) {
|
||||
String base = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString();
|
||||
ItemEnchantments enchants = enchantmentsForSpec(stack);
|
||||
if (enchants == null || enchants.isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
ArrayList<String> parts = new ArrayList<>();
|
||||
for (var e : enchants.entrySet()) {
|
||||
String name = e.getKey().getRegisteredName();
|
||||
String name = normalizeEnchantId(e.getKey().getRegisteredName());
|
||||
parts.add(name + "=" + e.getIntValue());
|
||||
}
|
||||
Collections.sort(parts);
|
||||
@@ -50,15 +60,16 @@ public final class TradeItemSpec {
|
||||
if (stack.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int sep = spec.indexOf(SPEC_SEP);
|
||||
String itemPart = sep < 0 ? spec : spec.substring(0, sep);
|
||||
String trimmed = spec.trim();
|
||||
int sep = trimmed.indexOf(SPEC_SEP);
|
||||
String itemPart = (sep < 0 ? trimmed : trimmed.substring(0, sep)).trim();
|
||||
if (!BuiltInRegistries.ITEM.getKey(stack.getItem()).toString().equals(itemPart)) {
|
||||
return false;
|
||||
}
|
||||
if (sep < 0) {
|
||||
return true;
|
||||
}
|
||||
Map<String, Integer> expected = parseEnchantSection(spec.substring(sep + 1));
|
||||
Map<String, Integer> expected = parseEnchantSection(trimmed.substring(sep + 1));
|
||||
if (expected == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -70,7 +81,7 @@ public final class TradeItemSpec {
|
||||
return false;
|
||||
}
|
||||
for (var e : actual.entrySet()) {
|
||||
String name = e.getKey().getRegisteredName();
|
||||
String name = normalizeEnchantId(e.getKey().getRegisteredName());
|
||||
int level = e.getIntValue();
|
||||
Integer want = expected.get(name);
|
||||
if (want == null || want != level) {
|
||||
@@ -89,27 +100,29 @@ public final class TradeItemSpec {
|
||||
}
|
||||
|
||||
private static Map<String, Integer> parseEnchantSection(String section) {
|
||||
if (section.isEmpty()) {
|
||||
String s = section.trim();
|
||||
if (s.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Integer> out = new HashMap<>();
|
||||
for (String piece : section.split("&")) {
|
||||
if (piece.isEmpty()) {
|
||||
for (String piece : s.split("&")) {
|
||||
String p = piece.trim();
|
||||
if (p.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int eq = piece.lastIndexOf('=');
|
||||
if (eq <= 0 || eq == piece.length() - 1) {
|
||||
int eq = p.lastIndexOf('=');
|
||||
if (eq <= 0 || eq == p.length() - 1) {
|
||||
return null;
|
||||
}
|
||||
String enchantId = piece.substring(0, eq);
|
||||
String levelStr = piece.substring(eq + 1);
|
||||
String enchantId = p.substring(0, eq).trim();
|
||||
String levelStr = p.substring(eq + 1).trim();
|
||||
int level;
|
||||
try {
|
||||
level = Integer.parseInt(levelStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
out.put(enchantId, level);
|
||||
out.put(normalizeEnchantId(enchantId), level);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
|
||||
"autotrade.message.sell_item_set": "Item to sell: %s",
|
||||
"autotrade.message.buy_item_set": "Item to buy: %s",
|
||||
"autotrade.message.input_container_set": "Input Container now at: %d %d %d",
|
||||
"autotrade.message.output_container_set": "Output Container now at: %d %d %d",
|
||||
"autotrade.message.input_container_set": "%s now set as Input Container: %s",
|
||||
"autotrade.message.output_container_set": "%s now set as Output Container: %s",
|
||||
"autotrade.message.container_pos_error": "Could not determine container position",
|
||||
|
||||
"autotrade.message.trade_bought": "Bought %s (price: %s)",
|
||||
"autotrade.message.trade_sold": "Sold %s for %s",
|
||||
|
||||
13
src/main/resources/autotrade.mixins.json
Normal file
13
src/main/resources/autotrade.mixins.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "com.github.sebseb7.autotrade.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"MultiPlayerGameModeInvoker",
|
||||
"MerchantMenuAccessor"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
|
||||
"mixins": [
|
||||
"autotrade.mixins.json"
|
||||
],
|
||||
|
||||
"depends": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
modstitch.platform=fabric-loom-remap
|
||||
deps.minecraft=1.21.10
|
||||
|
||||
fabric_loader_version=0.19.2
|
||||
fabric_loader_version=0.18.3
|
||||
fabric_api_version=0.138.4+1.21.10
|
||||
malilib_version=0.26.8
|
||||
mod_menu_version=16.0.1
|
||||
|
||||
@@ -2,7 +2,7 @@ modstitch.platform=fabric-loom-remap
|
||||
deps.minecraft=1.21.11
|
||||
|
||||
fabric_loader_version=0.19.2
|
||||
fabric_api_version=0.136.0+1.21.11
|
||||
malilib_version=0.27.3
|
||||
fabric_api_version=0.141.3+1.21.11
|
||||
malilib_version=0.27.9
|
||||
mod_menu_version=17.0.0
|
||||
minecraft_version_min=1.21.11
|
||||
|
||||
Reference in New Issue
Block a user