Refactor build system to support 1.21.10, 1.21.11, and 26.1.2

This commit is contained in:
seb
2026-05-10 20:55:01 +02:00
parent 2ac51f4aff
commit 256901ff00
22 changed files with 665 additions and 205 deletions

View File

@@ -7,15 +7,15 @@ jobs:
environment: modrinth
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v3
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Execute Gradle build
run: ./gradlew build
- 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

23
.gitignore vendored
View File

@@ -1,3 +1,24 @@
.gradle
# Gradle
.gradle/
build/
# Local JDK / toolchain installs (optional per-machine)
.jdk21/
# Fabric Loom dev client / game output
run/
runs/
# IDE
.idea/
*.iml
# OS
.DS_Store
Thumbs.db
# Logs & local scratch
*.log
out.txt
full_build.txt
compile_err.txt

View File

@@ -4,13 +4,13 @@
## Table of contents
1. [Description](#description)
2. [Build](#build-for-1203--1204)
2. [Build](#build)
3. [Known Issues](#known-issues)
4. [Possible Setup](#possible-setup)
5. [Void Trading Example & Settings](#void-trading-example--settings)
6. [WDL](#wdl)
feel free to ask questions: https://github.com/sebseb7/autotrade-fabric/discussions
Feel free to ask questions: https://github.com/sebseb7/autotrade-fabric/discussions
### Description
@@ -24,27 +24,49 @@ Beginning with version v0.0.10 you can select the sell/buy items using nametagge
From v0.0.13, those selections (and the set-buy / set-sell hotkeys) store exact enchantments when the stack is enchanted, so you can target a specific book or tool; a plain item id in config still matches any enchantment variant of that item.
if you can't access settings via the keybind, try modmenu https://modrinth.com/mod/modmenu
If you can't access settings via the keybind, try Mod Menu https://modrinth.com/mod/modmenu
#### Supported Version:
#### Supported versions (this branch)
- Minecraft 1.19.4 - 1.20.4 , 1.21.11, 26.1.2
Fabric targets maintained in-tree ([Stonecutter](https://stonecutter.kikugie.dev/) + [Modstitch](https://github.com/modunion/modstitch)):
### Build for 1.20.3 / 1.20.4
- **Minecraft 1.21.10** (`:1.21.10-fabric`)
- **Minecraft 1.21.11** (`:1.21.11-fabric`)
- **Minecraft 26.1.2** (`:26.1.2-fabric`)
```
./gradlew build
Older Minecraft releases (for example 1.19.x1.20.x) are not built from this multi-target setup; use an older release tag or branch if you need those versions.
### Build
Requirements: a JDK suitable for the target (the build uses Java **21** for 1.21.x and **25** for 26.x via Gradle toolchains).
Build **every** configured game version (recommended before you commit):
```bash
./gradlew chiseledBuild
```
#### Build for older minecraft versions:
If the parallel build is flaky, try:
```
./gradlew build -Pminecraft_version_out=1.20.2 -Pminecraft_version=1.20.2 -Pminecraft_version_min=1.20.2 -Pmalilib_version=0.17.0 -Pmod_menu_version=8.0.1 -Pmappings_version=1.20.2+build.4
./gradlew build -Pminecraft_version_out=1.20.1 -Pminecraft_version=1.20.1 -Pminecraft_version_min=1.20 -Pmalilib_version=0.16.1 -Pmod_menu_version=7.0.1 -Pmappings_version=1.20.1+build.10
./gradlew build -Pminecraft_version_out=1.19.4 -Pminecraft_version=1.19.4 -Pminecraft_version_min=1.19.4 -Pmalilib_version=0.15.2 -Pmod_menu_version=6.1.0 -Pmappings_version=1.19.4+build.2
```bash
./gradlew chiseledBuild --max-workers=1
```
#### Requires:
Build **one** Fabric target (jar ends up under that version subproject, e.g. `versions/1.21.11-fabric/build/libs/`):
```bash
./gradlew :1.21.10-fabric:build
./gradlew :1.21.11-fabric:build
./gradlew :26.1.2-fabric:build
```
When using Stonecutters active-project workflow, reset it before committing:
```bash
./gradlew resetActiveProject
```
#### Requires
- malilib

View File

@@ -1,86 +0,0 @@
plugins {
id 'net.fabricmc.fabric-loom' version '1.16-SNAPSHOT'
id 'com.diffplug.spotless' version '6.19.0'
id "com.modrinth.minotaur" version "2.+"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
exclusiveContent {
forRepository {
maven {
name = 'Modrinth'
url = 'https://api.modrinth.com/maven'
}
}
filter {
includeGroup 'maven.modrinth'
}
}
maven { url 'https://maven.terraformersmc.com/releases/' }
maven { url 'https://jitpack.io' }
flatDir {
dirs '.'
}
}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.fabric_loader_version}"
// Official 26.1 template uses `implementation` for fabric-api (not modImplementation).
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
implementation "maven.modrinth:malilib:${project.malilib_version}"
compileOnly "com.terraformersmc:modmenu:${project.mod_menu_version}"
}
group = project.group + "." + project.mod_id
base {
archivesName = project.mod_file_name + '-' + project.minecraft_version_out
}
version = project.mod_version
if (version.endsWith('-dev')) {
version += "." + new Date().format('yyyyMMdd.HHmmss')
}
processResources {
inputs.property "mod_version", project.mod_version
filesMatching("fabric.mod.json") {
expand([
"mod_version" : project.version,
"minecraft_version_min": project.property("minecraft_version_min"),
"malilib_version" : project.property("malilib_version")
])
}
}
tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8"
}
spotless {
java {
importOrder()
removeUnusedImports()
cleanthat()
eclipse()
formatAnnotations()
}
}
modrinth {
token = System.getenv("MODRINTH_TOKEN")
syncBodyFrom = rootProject.file("README.md").text
projectId = 'C1naQCmt'
// Loom 1.16+ with net.fabricmc.fabric-loom (unobfuscated MC) does not create remapJar; ship the standard jar
uploadFile = tasks.jar
gameVersions = ['26.1.2']
loaders = ['fabric']
dependencies = []
}

120
build.gradle.kts Normal file
View File

@@ -0,0 +1,120 @@
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id("dev.isxander.modstitch.base") version "0.8.4"
id("com.modrinth.minotaur") version "2.+"
}
repositories {
mavenCentral()
maven("https://maven.fabricmc.net/")
maven("https://api.modrinth.com/maven")
maven("https://maven.terraformersmc.com/releases/")
maven("https://jitpack.io")
}
val minecraft = findProperty("deps.minecraft")?.toString()
?: error("deps.minecraft must be set by the active Stonecutter target (see versions/*/gradle.properties).")
val javaLanguageVersion = when (minecraft) {
"1.21.10", "1.21.11" -> 21
"26.1.2" -> 25
else -> error("Add Java toolchain mapping for Minecraft $minecraft in build.gradle.kts.")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaLanguageVersion))
}
}
val rawModVersion = findProperty("mod_version")?.toString() ?: error("mod_version")
val modReleaseVersion = if (rawModVersion.endsWith("-dev")) {
"$rawModVersion.${DateTimeFormatter.ofPattern("yyyyMMdd.HHmmss").format(LocalDateTime.now())}"
} else {
rawModVersion
}
modstitch {
minecraftVersion = minecraft
metadata {
modId = findProperty("mod_id")?.toString() ?: error("mod_id")
modName = findProperty("mod_name")?.toString() ?: error("mod_name")
modVersion = modReleaseVersion
modGroup = findProperty("mod_group")?.toString() ?: error("mod_group")
modAuthor = findProperty("author")?.toString() ?: error("author")
replacementProperties.put("mod_author", findProperty("author")?.toString() ?: error("author"))
replacementProperties.put("mod_issue_tracker", "https://github.com/sebseb7/autotrade-fabric/issues")
replacementProperties.put("mod_sources", "https://github.com/sebseb7/autotrade-fabric")
replacementProperties.put("mod_homepage", "https://modrinth.com/mod/autotrade-fabric")
replacementProperties.put("malilib_version", findProperty("malilib_version")?.toString() ?: error("malilib_version"))
replacementProperties.put(
"fabric_api_dependency",
when (minecraft) {
"26.1.2" -> ">=0.145.0"
"1.21.11" -> ">=0.140.0"
"1.21.10" -> ">=0.136.0"
else -> error("fabric_api_dependency mapping for $minecraft")
},
)
replacementProperties.put(
"minecraft_dependency",
">=" + (findProperty("minecraft_version_min")?.toString() ?: minecraft),
)
}
loom {
fabricLoaderVersion = findProperty("fabric_loader_version")?.toString()
?: error("fabric_loader_version")
configureLoom {
}
}
}
val loader = project.name.substringAfterLast("-")
stonecutter {
consts(
"fabric" to loader.equals("fabric", ignoreCase = true),
"neoforge" to loader.equals("neoforge", ignoreCase = true),
"forge" to loader.equals("forge", ignoreCase = true),
"vanilla" to loader.equals("vanilla", ignoreCase = true),
"mc26" to (minecraft == "26.1.2"),
"npcSplit" to (minecraft == "26.1.2" || minecraft == "1.21.11"),
"npcFlat" to (minecraft == "1.21.10"),
)
}
dependencies {
val fabricApi = findProperty("fabric_api_version")?.toString() ?: error("fabric_api_version")
val malilib = findProperty("malilib_version")?.toString() ?: error("malilib_version")
val modMenu = findProperty("mod_menu_version")?.toString() ?: error("mod_menu_version")
modstitchModImplementation("net.fabricmc.fabric-api:fabric-api:$fabricApi")
modstitchModImplementation("maven.modrinth:malilib:$malilib")
modstitchModCompileOnly("com.terraformersmc:modmenu:$modMenu")
}
group = findProperty("mod_group")?.toString() ?: error("mod_group")
base {
archivesName.set("${findProperty("mod_file_name")}-$minecraft")
}
version = modReleaseVersion
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
}
modrinth {
token.set(System.getenv("MODRINTH_TOKEN"))
syncBodyFrom.set(rootProject.file("README.md").readText())
projectId.set("C1naQCmt")
uploadFile.set(tasks.jar)
gameVersions.addAll(listOf("26.1.2", "1.21.10", "1.21.11"))
loaders.add("fabric")
}

View File

@@ -3,20 +3,11 @@ org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
org.gradle.configuration-cache=false
group = com.github.sebseb7
mod_id = autotrade
mod_name = AutoTrade
author = sebseb7
mod_file_name = autotrade-fabric
group=com.github.sebseb7
mod_id=autotrade
mod_name=AutoTrade
mod_group=com.github.sebseb7.autotrade
author=sebseb7
mod_file_name=autotrade-fabric
mod_version = 0.0.14
malilib_version = 0.28.2
minecraft_version_min = 26.1.2
minecraft_version_out = 26.1.2
minecraft_version = 26.1.2
fabric_loader_version = 0.19.2
fabric_api_version = 0.145.4+26.1.2
mod_menu_version = 18.0.0-alpha.8
mod_version=0.0.14

View File

@@ -1,17 +0,0 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
// Lets Gradle 21 run the build while a JDK 25 toolchain is used to compile (required by 26.1 Mod Menu, etc.)
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'autotrade-fabric'

34
settings.gradle.kts Normal file
View File

@@ -0,0 +1,34 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
maven("https://maven.isxander.dev/releases/")
maven("https://maven.fabricmc.net/")
maven("https://maven.neoforged.net/releases/")
maven("https://maven.kikugie.dev/releases")
maven("https://maven.kikugie.dev/snapshots")
}
}
plugins {
id("dev.kikugie.stonecutter") version "0.6+"
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
stonecutter {
kotlinController = true
centralScript = "build.gradle.kts"
create(rootProject) {
fun mc(mcVersion: String, name: String = mcVersion, loaders: Iterable<String>) =
loaders.forEach { vers("$name-$it", mcVersion) }
mc("26.1.2", loaders = listOf("fabric"))
mc("1.21.10", loaders = listOf("fabric"))
mc("1.21.11", loaders = listOf("fabric"))
vcsVersion = "26.1.2-fabric"
}
}
rootProject.name = "autotrade-fabric"

View File

@@ -3,6 +3,7 @@ package com.github.sebseb7.autotrade;
import com.github.sebseb7.autotrade.config.Configs;
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;
@@ -19,6 +20,7 @@ public class InitHandler implements IInitializationHandler {
TraderHighlightRenderer.register();
VillagerTradeOverlayRenderer.register();
MerchantScreenButtonInjector.register();
InputHandler handler = new InputHandler();
InputEventHandler.getKeybindManager().registerKeybindProvider(handler);

View File

@@ -2,16 +2,21 @@ package com.github.sebseb7.autotrade.config;
import com.github.sebseb7.autotrade.Reference;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import fi.dy.masa.malilib.config.ConfigUtils;
import fi.dy.masa.malilib.config.IConfigHandler;
import fi.dy.masa.malilib.config.IConfigValue;
import fi.dy.masa.malilib.config.options.ConfigBoolean;
import fi.dy.masa.malilib.config.options.ConfigInteger;
import fi.dy.masa.malilib.config.options.ConfigString;
import fi.dy.masa.malilib.util.JsonUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import net.fabricmc.loader.api.FabricLoader;
public class Configs implements IConfigHandler {
@@ -60,25 +65,32 @@ public class Configs implements IConfigHandler {
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,
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);
SHOW_TRADES, SELECTED_ENCHANTMENTS);
}
public static void loadFromFile() {
File configFile = new File(getConfigDirectory(), CONFIG_FILE_NAME);
if (configFile.exists() && configFile.isFile() && configFile.canRead()) {
JsonElement element = JsonUtils.parseJsonFile(configFile.toPath());
try {
String json = Files.readString(configFile.toPath(), StandardCharsets.UTF_8);
JsonElement element = JsonParser.parseString(json);
if (element != null && element.isJsonObject()) {
JsonObject root = element.getAsJsonObject();
if (element != null && element.isJsonObject()) {
JsonObject root = element.getAsJsonObject();
ConfigUtils.readConfigBase(root, "Generic", Generic.OPTIONS);
ConfigUtils.readConfigBase(root, "Hotkeys", Hotkeys.HOTKEY_LIST);
ConfigUtils.readConfigBase(root, "Generic", Generic.OPTIONS);
ConfigUtils.readConfigBase(root, "Hotkeys", Hotkeys.HOTKEY_LIST);
}
} catch (IOException ignored) {
// Malformed or unreadable config; defaults stay active.
}
}
}
@@ -87,12 +99,16 @@ public class Configs implements IConfigHandler {
File dir = getConfigDirectory();
if ((dir.exists() && dir.isDirectory()) || dir.mkdirs()) {
JsonObject root = new JsonObject();
try {
JsonObject root = new JsonObject();
ConfigUtils.writeConfigBase(root, "Generic", Generic.OPTIONS);
ConfigUtils.writeConfigBase(root, "Hotkeys", Hotkeys.HOTKEY_LIST);
ConfigUtils.writeConfigBase(root, "Generic", Generic.OPTIONS);
ConfigUtils.writeConfigBase(root, "Hotkeys", Hotkeys.HOTKEY_LIST);
JsonUtils.writeJsonToFile(root, new File(dir, CONFIG_FILE_NAME).toPath());
Gson gson = new GsonBuilder().setPrettyPrinting().create();
Files.writeString(new File(dir, CONFIG_FILE_NAME).toPath(), gson.toJson(root), StandardCharsets.UTF_8);
} catch (IOException ignored) {
}
}
}

View File

@@ -19,8 +19,14 @@ 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;
@@ -33,7 +39,9 @@ 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;
final class AutoTradeClientTick {
@@ -144,8 +152,12 @@ final class AutoTradeClientTick {
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;
@@ -338,6 +350,7 @@ final class AutoTradeClientTick {
}
}
screen.onClose();
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
startTraderGlow(mc, villagerActive);
}

View File

@@ -9,7 +9,11 @@ import java.util.Map;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.player.Inventory;
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.Slot;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
@@ -20,7 +24,28 @@ final class ContainerIoHelper {
static void quickMoveResultSlot(Minecraft mc, AbstractContainerMenu menu, int slotIndex) {
Slot slot = menu.getSlot(slotIndex);
//? if mc26 {
mc.gameMode.handleContainerInput(menu.containerId, slot.index, 0, ContainerInput.QUICK_MOVE, mc.player);
//?} else {
mc.gameMode.handleInventoryMouseClick(menu.containerId, slot.index, 0, ClickType.QUICK_MOVE, mc.player);
//?}
}
/**
* 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.
*/
static void syncPlayerInventoryAfterMerchant(Minecraft mc) {
if (mc.player == null || mc.gameMode == null) {
return;
}
mc.gameMode.ensureHasSentCarriedItem();
mc.execute(() -> {
if (mc.player != null && mc.gameMode != null) {
mc.gameMode.ensureHasSentCarriedItem();
}
});
}
private static final String EMERALD_SPEC = "minecraft:emerald";

View File

@@ -0,0 +1,183 @@
package com.github.sebseb7.autotrade.gui;
import com.github.sebseb7.autotrade.config.Configs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import net.minecraft.client.Minecraft;
//? if mc26 {
import net.minecraft.client.gui.GuiGraphicsExtractor;
//?} else {
import net.minecraft.client.gui.GuiGraphics;
//?}
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Checkbox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.enchantment.Enchantment;
/**
* Full-screen GUI that lists every registered enchantment at every possible
* level with a checkbox. For example, Sharpness (max level 5) produces five
* rows: {@code minecraft:sharpness=1} through {@code minecraft:sharpness=5}.
*
* <p>
* Ticked entries are persisted into
* {@link Configs.Generic#SELECTED_ENCHANTMENTS} as a comma-separated list (e.g.
* {@code minecraft:sharpness=5,minecraft:mending=1}).
*/
public class EnchantmentSelectionScreen extends Screen {
private final Screen parent;
/**
* Every (enchantment, level) pair in alphabetical order. Each entry's
* {@link EnchantmentEntry#key} is the string that gets saved to config (e.g.
* {@code minecraft:sharpness=3}).
*/
private List<EnchantmentEntry> entries = List.of();
/** Currently selected set (mutable copy while the screen is open). */
private final Set<String> selected = new LinkedHashSet<>();
private static final int ROW_HEIGHT = 24;
private static final int LEFT_PADDING = 10;
/** Current scroll offset (in pixels). */
private int scrollOffset = 0;
/** Total content height (number of rows × ROW_HEIGHT). */
private int contentHeight = 0;
/** Checkboxes created for the current scroll window. */
private final List<CheckboxEntry> checkboxEntries = new ArrayList<>();
public EnchantmentSelectionScreen(Screen parent) {
super(Component.literal("Select Enchantments"));
this.parent = parent;
}
@Override
protected void init() {
super.init();
// Load currently-selected enchantments from config.
selected.clear();
String cfg = Configs.Generic.SELECTED_ENCHANTMENTS.getStringValue().trim();
if (!cfg.isEmpty()) {
Arrays.stream(cfg.split(",")).map(String::trim).filter(s -> !s.isEmpty()).forEach(selected::add);
}
// Build (enchantment, level) pairs from the dynamic registry.
entries = new ArrayList<>();
Minecraft mc = Minecraft.getInstance();
if (mc.level != null) {
Registry<Enchantment> registry = mc.level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
var ids = new ArrayList<>(registry.keySet());
ids.sort((a, b) -> a.toString().compareTo(b.toString()));
for (var id : ids) {
Enchantment ench = registry.getValue(id);
if (ench == null) {
continue;
}
int maxLevel = ench.getMaxLevel();
for (int level = 1; level <= maxLevel; level++) {
String key = id.toString() + "=" + level;
String label = id.toString() + " " + level;
entries.add(new EnchantmentEntry(key, label));
}
}
}
contentHeight = entries.size() * ROW_HEIGHT;
// "Done" button at the bottom.
this.addRenderableWidget(Button.builder(Component.literal("Done"), btn -> onDone())
.bounds(this.width / 2 - 100, this.height - 28, 200, 20).build());
rebuildCheckboxes();
}
/** Rebuild the checkbox widgets for the current scroll offset. */
private void rebuildCheckboxes() {
// Remove old checkboxes.
for (CheckboxEntry entry : checkboxEntries) {
this.removeWidget(entry.checkbox);
}
checkboxEntries.clear();
for (int i = 0; i < entries.size(); i++) {
int rowY = 30 + i * ROW_HEIGHT - scrollOffset;
if (rowY + ROW_HEIGHT < 30 || rowY > this.height - 36) {
continue; // off-screen
}
EnchantmentEntry e = entries.get(i);
boolean checked = selected.contains(e.key);
Checkbox cb = Checkbox.builder(Component.literal(e.label), this.font).pos(LEFT_PADDING, rowY)
.selected(checked).build();
this.addRenderableWidget(cb);
checkboxEntries.add(new CheckboxEntry(i, cb));
}
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
int maxScroll = Math.max(0, contentHeight - (this.height - 60));
scrollOffset = (int) Math.max(0, Math.min(maxScroll, scrollOffset - verticalAmount * ROW_HEIGHT));
syncCheckboxSelections();
rebuildCheckboxes();
return true;
}
/** Before rebuilding, read back checkbox state into our selected set. */
private void syncCheckboxSelections() {
for (CheckboxEntry entry : checkboxEntries) {
String key = entries.get(entry.index).key;
if (entry.checkbox.selected()) {
selected.add(key);
} else {
selected.remove(key);
}
}
}
//? if mc26 {
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
graphics.centeredText(this.font, this.title, this.width / 2, 10, 0xFFFFFF);
}
//?} else {
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
super.render(guiGraphics, mouseX, mouseY, partialTick);
guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, 10, 0xFFFFFF);
}
//?}
private void onDone() {
syncCheckboxSelections();
String value = selected.stream().sorted().collect(Collectors.joining(","));
Configs.Generic.SELECTED_ENCHANTMENTS.setValueFromString(value);
Configs.saveToFile();
this.minecraft.setScreen(parent);
}
@Override
public void onClose() {
onDone();
}
/** An enchantment at a specific level. */
private record EnchantmentEntry(String key, String label) {
}
/** Pairs a checkbox widget with its index in {@link #entries}. */
private record CheckboxEntry(int index, Checkbox checkbox) {
}
}

View File

@@ -0,0 +1,93 @@
package com.github.sebseb7.autotrade.gui;
import com.github.sebseb7.autotrade.render.VillagerTradeCache;
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.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
import net.minecraft.network.chat.Component;
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.item.trading.MerchantOffers;
import net.minecraft.world.phys.AABB;
public final class MerchantScreenButtonInjector {
private MerchantScreenButtonInjector() {
}
public static void register() {
ScreenEvents.AFTER_INIT.register(MerchantScreenButtonInjector::onScreenInit);
}
private static void onScreenInit(Minecraft client, Screen screen, int scaledWidth, int scaledHeight) {
if (!(screen instanceof MerchantScreen merchantScreen)) {
return;
}
// 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);
//?} else {
Screens.getButtons(asScreen).add(button);
//?}
// 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};
ScreenEvents.afterTick(merchantScreen).register(s -> {
if (handled[0]) {
return;
}
MerchantOffers offers = merchantScreen.getMenu().getOffers();
if (offers == null || offers.isEmpty()) {
return; // not yet synced
}
handled[0] = true;
cacheOffersForNearestTrader(client, offers);
});
}
private static void cacheOffersForNearestTrader(Minecraft mc, MerchantOffers offers) {
if (mc.player == null || mc.level == null) {
return;
}
AABB searchBox = mc.player.getBoundingBox().inflate(10.0);
List<Entity> nearby = mc.level.getEntitiesOfClass(Entity.class, searchBox);
Entity closest = null;
double closestDist = Double.MAX_VALUE;
for (Entity entity : nearby) {
if (entity instanceof Villager || entity instanceof WanderingTrader) {
double dist = entity.distanceToSqr(mc.player);
if (dist < closestDist) {
closestDist = dist;
closest = entity;
}
}
}
if (closest != null) {
VillagerTradeCache.put(closest.getUUID(), offers);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.github.sebseb7.autotrade.render;
//? if mc26 {
import com.github.sebseb7.autotrade.config.Configs;
import com.github.sebseb7.autotrade.event.KeybindCallbacks;
import com.mojang.blaze3d.vertex.PoseStack;
@@ -16,6 +17,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.Shapes;
//?}
/**
* Client wireframe highlights: last-traded villager, and input/output container
@@ -23,6 +25,17 @@ import net.minecraft.world.phys.shapes.Shapes;
* boxes).
*/
public final class TraderHighlightRenderer {
private TraderHighlightRenderer() {
}
public static void register() {
//? if mc26 {
LevelRenderEvents.AFTER_SOLID_FEATURES.register(TraderHighlightRenderer::renderLevel);
//?}
}
//? if mc26 {
private static final ShapeRenderer SHAPE_RENDERER = new ShapeRenderer();
private static final int TRADER_OUTLINE_COLOR = 0xFF66FF66;
@@ -31,14 +44,7 @@ public final class TraderHighlightRenderer {
private static final float LINE_WIDTH = 2.5F;
private TraderHighlightRenderer() {
}
public static void register() {
LevelRenderEvents.AFTER_SOLID_FEATURES.register(TraderHighlightRenderer::render);
}
private static void render(LevelRenderContext context) {
private static void renderLevel(LevelRenderContext context) {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) {
return;
@@ -57,14 +63,18 @@ public final class TraderHighlightRenderer {
Vec3 camera = mc.gameRenderer.getMainCamera().position();
float tickDelta = mc.getDeltaTracker().getGameTimeDeltaPartialTick(true);
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) {
if (trader != null) {
double offX = Mth.lerp(tickDelta, trader.xOld, trader.getX()) - trader.getX();
double offY = Mth.lerp(tickDelta, trader.yOld, trader.getY()) - trader.getY();
double offZ = Mth.lerp(tickDelta, trader.zOld, trader.getZ()) - trader.getZ();
AABB worldBox = trader.getBoundingBox().move(offX, offY, offZ);
AABB cameraRelative = worldBox.move(-camera.x, -camera.y, -camera.z);
SHAPE_RENDERER.renderShape(drawPose, consumer, Shapes.create(cameraRelative), 0.0D, 0.0D, 0.0D,
TRADER_OUTLINE_COLOR, LINE_WIDTH);
renderShape(drawPose, consumer, cameraRelative, TRADER_OUTLINE_COLOR);
}
if (inTicks > 0) {
@@ -86,7 +96,12 @@ public final class TraderHighlightRenderer {
int color) {
AABB world = AABB.encapsulatingFullBlocks(pos, pos);
AABB cameraRelative = world.move(-camera.x, -camera.y, -camera.z);
renderShape(drawPose, consumer, cameraRelative, color);
}
private static void renderShape(PoseStack drawPose, VertexConsumer consumer, AABB cameraRelative, int color) {
SHAPE_RENDERER.renderShape(drawPose, consumer, Shapes.create(cameraRelative), 0.0D, 0.0D, 0.0D, color,
LINE_WIDTH);
}
//?}
}

View File

@@ -9,7 +9,8 @@ import net.minecraft.world.item.trading.MerchantOffers;
* Client-side cache of villager/wandering-trader trade offers, keyed by entity
* UUID.
*
* <p>Populated by {@code AutoTradeClientTick} when the mod opens a merchant
* <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.
*/
@@ -24,7 +25,10 @@ public final class VillagerTradeCache {
CACHE.put(entityUuid, offers);
}
/** Retrieve cached offers, or {@code null} if we haven't seen this entity trade yet. */
/**
* Retrieve cached offers, or {@code null} if we haven't seen this entity trade
* yet.
*/
public static MerchantOffers get(UUID entityUuid) {
return CACHE.get(entityUuid);
}

View File

@@ -2,34 +2,36 @@ package com.github.sebseb7.autotrade.render;
import com.github.sebseb7.autotrade.config.Configs;
import com.mojang.blaze3d.vertex.PoseStack;
import java.util.ArrayList;
import java.util.List;
//? 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.client.renderer.MultiBufferSource;
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.
* <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 {
/** Vertical gap between successive trade lines (in world-space blocks). */
private static final float LINE_SPACING = 0.25F;
/** World-space scale of the text (vanilla name-tags use ~0.025). */
private static final float TEXT_SCALE = 0.02F;
@@ -46,10 +48,13 @@ public final class VillagerTradeOverlayRenderer {
}
public static void register() {
LevelRenderEvents.AFTER_SOLID_FEATURES.register(VillagerTradeOverlayRenderer::render);
//? if mc26 {
LevelRenderEvents.COLLECT_SUBMITS.register(VillagerTradeOverlayRenderer::renderLevel);
//?}
}
private static void render(LevelRenderContext context) {
//? if mc26 {
private static void renderLevel(LevelRenderContext context) {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null || mc.player == null) {
return;
@@ -59,7 +64,6 @@ public final class VillagerTradeOverlayRenderer {
}
Font font = mc.font;
MultiBufferSource.BufferSource bufferSource = context.bufferSource();
Vec3 camera = mc.gameRenderer.getMainCamera().position();
float tickDelta = mc.getDeltaTracker().getGameTimeDeltaPartialTick(true);
@@ -67,7 +71,6 @@ public final class VillagerTradeOverlayRenderer {
if (!(entity instanceof Villager) && !(entity instanceof WanderingTrader)) {
continue;
}
// Only render for villagers within a reasonable distance.
if (entity.distanceToSqr(mc.player) > 64.0 * 64.0) {
continue;
}
@@ -77,29 +80,24 @@ public final class VillagerTradeOverlayRenderer {
continue;
}
// Build compact trade lines: "CostA [+ CostB] → Result (uses/max)"
List<TradeLineEntry> lines = buildTradeLines(offers);
if (lines.isEmpty()) {
continue;
}
// Interpolated entity position relative to camera.
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;
// Place the first line above the entity's head (entity height + small gap).
float baseY = entity.getBbHeight() + 0.6F;
PoseStack poseStack = new PoseStack();
poseStack.pushPose();
poseStack.translate(x, y + baseY, z);
// Face the camera (billboard).
poseStack.mulPose(mc.gameRenderer.getMainCamera().rotation());
poseStack.scale(-TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE);
// Draw lines from top (highest index) to bottom (index 0).
for (int i = 0; i < lines.size(); i++) {
TradeLineEntry entry = lines.get(i);
float lineOffsetY = -(lines.size() - 1 - i) * (font.lineHeight + 2);
@@ -111,46 +109,38 @@ public final class VillagerTradeOverlayRenderer {
int textWidth = font.width(entry.text);
float textX = -textWidth / 2.0F;
// Background
font.drawInBatch(entry.text, textX, 0, entry.color, false, matrix, bufferSource,
Font.DisplayMode.SEE_THROUGH, BG_COLOR, 0xF000F0);
// Foreground
font.drawInBatch(entry.text, textX, 0, entry.color, false, matrix, bufferSource,
Font.DisplayMode.NORMAL, 0, 0xF000F0);
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();
// Cost A
if (!offer.getCostA().isEmpty()) {
sb.append(offer.getCostA().getCount()).append("× ")
.append(offer.getCostA().getHoverName().getString());
sb.append(offer.getCostA().getCount()).append("× ").append(offer.getCostA().getHoverName().getString());
}
// Cost B (optional)
if (!offer.getCostB().isEmpty()) {
if (sb.length() > 0) {
sb.append(" + ");
}
sb.append(offer.getCostB().getCount()).append("× ")
.append(offer.getCostB().getHoverName().getString());
sb.append(offer.getCostB().getCount()).append("× ").append(offer.getCostB().getHoverName().getString());
}
sb.append("");
// Result
sb.append(offer.getResult().getCount()).append("× ")
.append(offer.getResult().getHoverName().getString());
sb.append(offer.getResult().getCount()).append("× ").append(offer.getResult().getHoverName().getString());
// Remaining uses
int remaining = offer.getMaxUses() - offer.getUses();
sb.append(" (").append(remaining).append("/").append(offer.getMaxUses()).append(")");

View File

@@ -1,17 +1,17 @@
{
"schemaVersion": 1,
"id": "autotrade",
"name": "Auto Trade",
"id": "${mod_id}",
"name": "${mod_name}",
"version": "${mod_version}",
"description": "AFK trade with villagers",
"authors": [
"sebseb7"
"${mod_author}"
],
"contact": {
"homepage": "https://modrinth.com/mod/autotrade-fabric",
"issues": "https://github.com/sebseb7/autotrade-fabric/issues",
"sources": "https://github.com/sebseb7/autotrade-fabric"
"homepage": "${mod_homepage}",
"issues": "${mod_issue_tracker}",
"sources": "${mod_sources}"
},
"license": "0BSD",
@@ -30,8 +30,8 @@
],
"depends": {
"minecraft": ">=${minecraft_version_min}",
"minecraft": "${minecraft_dependency}",
"malilib": ">=${malilib_version}",
"fabric-api": ">=0.145.0"
"fabric-api": "${fabric_api_dependency}"
}
}

10
stonecutter.gradle.kts Normal file
View File

@@ -0,0 +1,10 @@
plugins {
id("dev.kikugie.stonecutter")
}
stonecutter active "26.1.2-fabric"
stonecutter registerChiseled tasks.register("chiseledBuild", stonecutter.chiseled) {
group = "project"
ofTask("build")
}

View File

@@ -0,0 +1,8 @@
modstitch.platform=fabric-loom-remap
deps.minecraft=1.21.10
fabric_loader_version=0.19.2
fabric_api_version=0.138.4+1.21.10
malilib_version=0.26.8
mod_menu_version=16.0.1
minecraft_version_min=1.21.10

View File

@@ -0,0 +1,8 @@
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
mod_menu_version=17.0.0
minecraft_version_min=1.21.11

View File

@@ -0,0 +1,8 @@
modstitch.platform=fabric-loom
deps.minecraft=26.1.2
fabric_loader_version=0.19.2
fabric_api_version=0.145.4+26.1.2
malilib_version=0.28.2
mod_menu_version=18.0.0-alpha.8
minecraft_version_min=26.1.2