Initial commit: Villager Roller v1.0.0
Build Mod / build (push) Failing after 6s

This commit is contained in:
jkortis committed 2026-09-30 23:11:02 -04:00
commit fa95e7781e
32 files changed
+2545

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# Auto-detect text files and normalize line endings
* text=auto
# Ensure shell scripts and gradlew wrapper always keep LF line endings for Linux/macOS & CI
gradlew text eol=lf
*.sh text eol=lf
# Windows batch scripts
*.bat text eol=crlf
*.cmd text eol=crlf
# Binary files
*.jar binary
*.png binary
*.ico binary
+48
View File
@@ -0,0 +1,48 @@
name: Build Mod
on:
push:
branches: [ "main", "master" ]
tags:
- 'v*'
pull_request:
branches: [ "main", "master" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: villager-roller-mod
path: build/libs/*.jar
- name: Create Gitea Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
build/libs/*.jar
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+48
View File
@@ -0,0 +1,48 @@
name: Build Mod
on:
push:
branches: [ "main", "master" ]
tags:
- 'v*'
pull_request:
branches: [ "main", "master" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: villager-roller-mod
path: build/libs/*.jar
- name: Create Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
build/libs/*.jar
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+24
View File
@@ -0,0 +1,24 @@
# Gradle build & cache
.gradle/
build/
bin/
out/
# Minecraft client logs and runs
run/
logs/
crash-reports/
*.log
*.log.gz
# IDE & Editor Files
.idea/
*.iml
.vscode/
.classpath
.project
.settings/
# OS specific files
.DS_Store
Thumbs.db
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Villager Roller Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+126
View File
@@ -0,0 +1,126 @@
# Villager Roller
[![Minecraft](https://img.shields.io/badge/Minecraft-26.3-brightgreen.svg)](https://minecraft.net/)
[![Fabric](https://img.shields.io/badge/Fabric-0.19.5+-blue.svg)](https://fabricmc.net/)
[![Java](https://img.shields.io/badge/Java-25-orange.svg)](https://adoptium.net/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
**Villager Roller** is a high-performance, client-side Fabric mod for Minecraft 26.3 that automates the tedious process of rerolling villager trades. By breaking and placing workstations, opening trade interfaces, and analyzing offers automatically, it lets you secure your desired enchanted books and commodity trades with ease.
---
## ✨ Features
- **🤖 Fully Automated Rerolling**: Automates breaking the workstation block, placing it back down, awaiting job claiming, and reading offers.
- **📚 All 13 Workstations Supported**: Includes Lectern, Blast Furnace, Smoker, Fletching Table, Composter, Smithing Table, Grindstone, Brewing Stand, Barrel, Cartography Table, Cauldron, Stonecutter, and Loom.
- **💎 Emerald Price Filtering**: Cycle max price thresholds (`≤10`, `≤15`, `≤20`, `≤32`, `Any ≤64`) so you never overpay for high-tier enchantments like Mending or Unbreaking III.
- **🖥️ In-Game HUD Overlay**: Displays current state, target trade, price limit, attempt counter, and background focus status in real time.
- **🪟 Background Focus Mode**: Toggle focus mode (<kbd>U</kbd>) to keep the game active and rolling even when alt-tabbed to another monitor or application.
- **🛑 One-Touch Emergency Stop**: Hit <kbd>X</kbd> anytime to instantly halt automation and restore normal controls.
- **🌐 Multiplayer & Latency Resilient**: Built-in state machine waits for server block confirmation and handles packet latency without desyncing.
- **⚙️ ModMenu & Keybinds Integration**: Access configuration and rebinding screens directly from ModMenu or in-game menus.
---
## 🎮 Default Keybindings
| Key | Default | Description |
|---|:---:|---|
| **Open Trade Selector** | <kbd>V</kbd> | Opens the trade selection and configuration menu |
| **Emergency Stop** | <kbd>X</kbd> | Instantly halts automation at any point |
| **Toggle Background Focus** | <kbd>U</kbd> | Toggles whether Minecraft continues running when unfocused |
> *All keybinds can be reconfigured in Options → Controls → Key Binds or via the in-game Keybinds screen.*
---
## 🚀 How to Use
### 1. Select Your Target Trade
1. Press <kbd>V</kbd> to open the **Trade Selector**.
2. Select your workstation on the left (e.g. *Lectern (Librarian)*).
3. Choose your desired trade on the right (e.g. *Mending*, *Unbreaking III*, *Efficiency V*, *32 Sticks -> 1 Emerald*).
4. *(Optional)* Click **Max Emeralds** to set a price ceiling (e.g. `≤ 15 Emeralds`).
5. Click **Confirm & Start Setup**.
### 2. Interactive 3-Step Calibration
The mod guides you through an initial setup sequence to ensure accurate positions:
- **Step 1/3**: Right-click your unlinked / novice villager.
- **Step 2/3**: Break and replace the villager's workstation block.
- **Step 3/3**: Right-click the villager to verify trade opening.
### 3. Automatic Takeover
- The state machine takes over automatically, cycling the workstation and inspecting trades.
- When your matching trade and price conditions are met:
- Automation stops immediately.
- An audible chime alerts you that the roll succeeded!
---
## 🏛️ Supported Workstations & Professions
| Workstation | Profession | Common Targets |
|---|---|---|
| **Lectern** | Librarian | Mending, Unbreaking III, Protection IV, Efficiency V, Fortune III, Silk Touch, Paper -> Emerald |
| **Fletching Table** | Fletcher | 32 Sticks -> 1 Emerald, Arrows, Flint |
| **Composter** | Farmer | Wheat, Potatoes, Carrots, Beetroots, Bread |
| **Blast Furnace** | Armorer | Coal -> Emerald, Iron Armor (Helmet, Chestplate, Leggings, Boots) |
| **Smithing Table** | Toolsmith | Coal -> Emerald, Iron Tools (Pickaxe, Axe, Shovel, Hoe) |
| **Grindstone** | Weaponsmith | Coal -> Emerald, Iron Sword, Iron Axe |
| **Smoker** | Butcher | Raw Chicken, Porkchop, Rabbit -> Emerald, Cooked Food |
| **Brewing Stand** | Cleric | 32 Rotten Flesh -> 1 Emerald, Redstone Dust |
| **Barrel** | Fisherman | Raw Cod, Raw Salmon, Campfire |
| **Cartography Table** | Cartographer | 24 Paper -> 1 Emerald, Blank Map |
| **Cauldron** | Leatherworker | Leather -> Emerald, Leather Armor |
| **Stonecutter** | Mason | 10 Clay Balls -> 1 Emerald, Bricks |
| **Loom** | Shepherd | 18 Wool -> 1 Emerald, Shears |
---
## 📥 Installation
1. Install **[Fabric Loader](https://fabricmc.net/use/)** (`0.19.5` or newer) for Minecraft `26.3`.
2. Download **[Fabric API](https://modrinth.com/mod/fabric-api)** (`0.161.0+26.3` or compatible).
3. Download the latest `villager-roller-1.0.0.jar` from the [Releases](../../releases) tab (or find it in the `release/` folder).
4. Place both `.jar` files into your `.minecraft/mods` folder.
5. Launch Minecraft using your Fabric profile.
---
## 🛠️ Building from Source
### Requirements
- **JDK 25** or newer
- Internet connection (for Gradle dependency resolution)
### Build Commands
Clone or download the repository, open a terminal in the project directory, and execute:
```bash
# Windows
.\gradlew clean build
# macOS / Linux
chmod +x gradlew
./gradlew clean build
```
The compiled mod JAR will be output to:
```
build/libs/villager-roller-1.0.0.jar
```
---
## 📂 Project Organization
This repository is maintained with a clean release workspace layout:
- **`src/`**: Mod source code, Mixins, UI components, and localization files.
- **`release/`**: Ready-to-use compiled release `.jar` binaries for easy distribution.
- **`.gitea/workflows/` & `.github/workflows/`**: Automated CI pipelines that build, test, and package release artifacts on every push or tag.
---
## 📄 License
This project is open source and available under the [MIT License](LICENSE).
+45
View File
@@ -0,0 +1,45 @@
plugins {
id 'net.fabricmc.fabric-loom' version "${loom_version}"
}
version = project.version
group = project.group
repositories {
mavenCentral()
maven {
name = "Terraformers"
url = "https://maven.terraformersmc.com/releases/"
}
}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
include "net.fabricmc.fabric-api:fabric-key-mapping-api-v1:2.0.8+3434d6d95d"
include "net.fabricmc.fabric-api:fabric-lifecycle-events-v1:4.1.9+ffef5f675d"
include "net.fabricmc.fabric-api:fabric-events-interaction-v0:5.3.6+3434d6d95d"
include "net.fabricmc.fabric-api:fabric-api-base:2.0.6+fcdff87f5d"
compileOnly "com.terraformersmc:modmenu:21.0.0"
}
processResources {
def version = project.version
inputs.property "version", version
filesMatching("fabric.mod.json") {
expand "version": version
}
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 25
}
java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
+14
View File
@@ -0,0 +1,14 @@
org.gradle.jvmargs=-Xmx2G
org.gradle.parallel=true
# Fabric Properties
minecraft_version=26.3
loader_version=0.19.5
loom_version=1.17.21
# Mod Properties
version=1.0.0
group=com.villagerroller
# Dependencies
fabric_api_version=0.161.0+26.3
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+112
View File
@@ -0,0 +1,112 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
@rem Catch executions from older scripts and ensure they exit cleanly.
@rem This can be removed once we can be reasonably confident that few people
@rem will be migrating directly to this new wrapper.
goto afterSafetyNet
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto exitWithErrorLevel
:afterSafetyNet
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
1>&2 echo.
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
1>&2 echo.
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
1>&2 echo.
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
1>&2 echo.
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
gradlePluginPortal()
}
}
rootProject.name = 'villager-roller'
@@ -0,0 +1,107 @@
package com.villagerroller;
import com.mojang.blaze3d.platform.InputConstants;
import com.villagerroller.automation.RerollStateMachine;
import com.villagerroller.ui.RerollScreen;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.UseEntityCallback;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.npc.villager.Villager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class VillagerRoller implements ClientModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("villager-roller");
public static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(
net.minecraft.resources.Identifier.fromNamespaceAndPath("villagerroller", "general")
);
public static KeyMapping openGuiKey;
public static KeyMapping stopKey;
public static KeyMapping toggleFocusKey;
private static RerollStateMachine stateMachine;
@Override
public void onInitializeClient() {
LOGGER.info("Villager Roller initializing...");
openGuiKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.villagerroller.open",
InputConstants.KEY_V,
CATEGORY
));
stopKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.villagerroller.stop",
InputConstants.KEY_X,
CATEGORY
));
toggleFocusKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.villagerroller.toggle_focus",
InputConstants.KEY_U,
CATEGORY
));
stateMachine = new RerollStateMachine();
// 1. Capture villager click during setup
UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> {
if (world.isClientSide() && entity instanceof Villager villager) {
stateMachine.onVillagerSelected(villager);
}
return InteractionResult.PASS;
});
// 2. Capture targeted workstation block during setup
AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> {
if (world.isClientSide()) {
stateMachine.onBlockAttacked(pos);
}
return InteractionResult.PASS;
});
// 3. Client tick handler for keybinds and state machine
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.player == null) return;
if (com.villagerroller.config.ModConfig.keepFocusedOnUnfocus && client.options != null && client.options.pauseOnLostFocus) {
client.options.pauseOnLostFocus = false;
}
while (openGuiKey.consumeClick()) {
client.execute(() -> client.setScreenAndShow(new RerollScreen(stateMachine)));
}
while (stopKey.consumeClick()) {
stateMachine.emergencyStop();
}
while (toggleFocusKey.consumeClick()) {
com.villagerroller.config.ModConfig.keepFocusedOnUnfocus = !com.villagerroller.config.ModConfig.keepFocusedOnUnfocus;
if (client.player != null) {
String status = com.villagerroller.config.ModConfig.keepFocusedOnUnfocus ? "§aENABLED" : "§cDISABLED";
client.player.sendSystemMessage(Component.literal("§6[Villager Roller] §eBackground Focus Mode: " + status));
client.getSoundManager().play(SimpleSoundInstance.forUI(
SoundEvents.UI_BUTTON_CLICK,
com.villagerroller.config.ModConfig.keepFocusedOnUnfocus ? 1.2F : 0.8F
));
}
}
stateMachine.tick(client);
});
}
public static RerollStateMachine getStateMachine() {
return stateMachine;
}
}
@@ -0,0 +1,743 @@
package com.villagerroller.automation;
import com.villagerroller.trade.TradeCategory;
import com.villagerroller.trade.TradeOption;
import com.villagerroller.util.TradeMatcher;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.inventory.MerchantMenu;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.ItemEnchantments;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.blaze3d.platform.Window;
import net.minecraft.client.MouseHandler;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
import net.minecraft.network.protocol.game.ServerboundSelectTradePacket;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.inventory.ContainerInput;
import net.minecraft.world.phys.AABB;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
public class RerollStateMachine {
public enum AutomationState {
IDLE,
SETUP_WAITING_FOR_VILLAGER,
SETUP_WAITING_FOR_WORKSTATION_BREAK,
SETUP_WAITING_FOR_WORKSTATION_PLACE,
SETUP_WAITING_FOR_INITIAL_CHECK,
AUTO_BREAKING,
AUTO_WAITING_BREAK,
AUTO_RETRIEVING_WORKSTATION,
AUTO_RETURNING_TO_STATION,
AUTO_WAITING_UNEMPLOY,
AUTO_PLACING,
AUTO_WAITING_PLACE,
AUTO_WAITING_CLAIM,
AUTO_INTERACTING,
AUTO_WAITING_OFFERS,
SUCCESS,
ERROR
}
private AutomationState currentState = AutomationState.IDLE;
private TradeCategory targetCategory;
private TradeOption targetOption;
private int maxEmeraldPrice = 64;
private Villager targetVillager;
private BlockPos workstationPos;
private Item workstationItem;
private Vec3 originalPlayerPos = null;
private boolean hasOnlyOneWorkstation = false;
private int tickCounter = 0;
private int attemptCount = 0;
private int stepCooldown = 0;
private String lastError = "";
private int matchedTradeIndex = -1;
private boolean mousePositioned = false;
public void startInteractiveSetup(TradeCategory category, TradeOption option, int maxPrice) {
this.targetCategory = category;
this.targetOption = option;
this.maxEmeraldPrice = maxPrice;
this.targetVillager = null;
this.workstationPos = null;
this.workstationItem = (category != null) ? category.getWorkstationItem() : null;
this.originalPlayerPos = null;
this.hasOnlyOneWorkstation = false;
this.attemptCount = 0;
this.tickCounter = 0;
this.stepCooldown = 15;
this.lastError = "";
this.matchedTradeIndex = -1;
this.mousePositioned = false;
this.currentState = AutomationState.SETUP_WAITING_FOR_VILLAGER;
playSound(SoundEvents.EXPERIENCE_ORB_PICKUP);
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
player.sendSystemMessage(Component.literal("§6[Villager Roller] §eTarget trade: §6" + option.getDisplayName() + " §7(Max: " + maxPrice + " Emeralds)"));
player.sendSystemMessage(Component.literal("§6[Villager Roller] §eStep 1/3: Right-click your villager to select them."));
}
}
public void onVillagerSelected(Villager villager) {
if (currentState != AutomationState.SETUP_WAITING_FOR_VILLAGER) return;
if (stepCooldown > 0) return;
this.targetVillager = villager;
this.currentState = AutomationState.SETUP_WAITING_FOR_WORKSTATION_BREAK;
this.workstationPos = null;
this.stepCooldown = 20;
this.tickCounter = 0;
playSound(SoundEvents.EXPERIENCE_ORB_PICKUP);
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
player.sendSystemMessage(Component.literal("§6[Villager Roller] §aVillager selected! §eStep 2/3: Destroy the trade station block."));
}
}
public void onBlockAttacked(BlockPos pos) {
if (currentState == AutomationState.SETUP_WAITING_FOR_WORKSTATION_BREAK) {
this.workstationPos = pos.immutable();
LocalPlayer player = Minecraft.getInstance().player;
if (player != null && Minecraft.getInstance().level != null) {
BlockState state = Minecraft.getInstance().level.getBlockState(pos);
int bestSlot = findBestToolSlot(player, state);
if (bestSlot != -1 && bestSlot != player.getInventory().getSelectedSlot()) {
player.getInventory().setSelectedSlot(bestSlot);
}
}
}
}
public void emergencyStop() {
releaseMovementKeys(Minecraft.getInstance());
if (Minecraft.getInstance().gameMode != null) {
Minecraft.getInstance().gameMode.stopDestroyBlock();
}
if (currentState != AutomationState.IDLE) {
this.currentState = AutomationState.IDLE;
this.targetVillager = null;
this.workstationPos = null;
this.originalPlayerPos = null;
this.hasOnlyOneWorkstation = false;
this.matchedTradeIndex = -1;
this.mousePositioned = false;
playSound(SoundEvents.EXPERIENCE_ORB_PICKUP);
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
player.sendSystemMessage(Component.literal("§c[Villager Roller] Automation stopped."));
}
}
}
public void tick(Minecraft client) {
if (currentState == AutomationState.IDLE || currentState == AutomationState.ERROR) {
return;
}
if (currentState == AutomationState.SUCCESS) {
// Once the matching trade screen has opened and the player closes it, release everything to IDLE
if (mousePositioned && client.gui.screen() == null) {
this.currentState = AutomationState.IDLE;
this.targetVillager = null;
this.workstationPos = null;
this.originalPlayerPos = null;
this.matchedTradeIndex = -1;
this.mousePositioned = false;
if (client.gameMode != null) {
client.gameMode.stopDestroyBlock();
}
releaseMovementKeys(client);
if (client.mouseHandler != null && !client.mouseHandler.isMouseGrabbed()) {
client.mouseHandler.grabMouse();
}
return;
}
if (!mousePositioned && matchedTradeIndex != -1 && client.gui.screen() instanceof MerchantScreen) {
selectTradeAndPositionMouse(client, matchedTradeIndex);
mousePositioned = true;
if (client.gameMode != null) {
client.gameMode.stopDestroyBlock();
}
releaseMovementKeys(client);
}
return;
}
LocalPlayer player = client.player;
if (player == null) return;
// Step 1: Wait for player to click on villager
if (currentState == AutomationState.SETUP_WAITING_FOR_VILLAGER) {
if (stepCooldown > 0) stepCooldown--;
player.sendOverlayMessage(Component.literal("§e[Villager Roller] Step 1/3: Right-click your villager"));
return;
}
// Step 2a: Wait for player to break the workstation
if (currentState == AutomationState.SETUP_WAITING_FOR_WORKSTATION_BREAK) {
if (stepCooldown > 0) stepCooldown--;
if (workstationPos == null) {
player.sendOverlayMessage(Component.literal("§e[Villager Roller] Step 2/3: Destroy the trade station block"));
} else {
player.sendOverlayMessage(Component.literal("§e[Villager Roller] Destroying workstation at " + workstationPos.getX() + ", " + workstationPos.getY() + ", " + workstationPos.getZ()));
if (client.level != null && client.level.getBlockState(workstationPos).isAir()) {
this.currentState = AutomationState.SETUP_WAITING_FOR_WORKSTATION_PLACE;
this.stepCooldown = 20;
this.tickCounter = 0;
playSound(SoundEvents.EXPERIENCE_ORB_PICKUP);
player.sendSystemMessage(Component.literal("§6[Villager Roller] §aWorkstation destroyed! §eNow place it back in the same spot."));
}
}
return;
}
// Step 2b: Wait for player to place the workstation back
if (currentState == AutomationState.SETUP_WAITING_FOR_WORKSTATION_PLACE) {
player.sendOverlayMessage(Component.literal("§e[Villager Roller] Step 2/3: Place the trade station back"));
if (stepCooldown > 0) {
stepCooldown--;
return;
}
if (workstationPos != null && client.level != null && !client.level.getBlockState(workstationPos).isAir()) {
BlockState placedState = client.level.getBlockState(workstationPos);
this.workstationItem = placedState.getBlock().asItem();
this.currentState = AutomationState.SETUP_WAITING_FOR_INITIAL_CHECK;
this.stepCooldown = 25;
this.tickCounter = 0;
int invCount = countWorkstationsInInventory(player);
this.hasOnlyOneWorkstation = (invCount == 0);
playSound(SoundEvents.EXPERIENCE_ORB_PICKUP);
if (hasOnlyOneWorkstation) {
player.sendSystemMessage(Component.literal("§6[Villager Roller] §aWorkstation placed! §e(Single workstation detected - auto-retrieval enabled)"));
} else {
player.sendSystemMessage(Component.literal("§6[Villager Roller] §aWorkstation placed! §e(" + (invCount + 1) + " workstations detected)"));
}
player.sendSystemMessage(Component.literal("§6[Villager Roller] §eStep 3/3: Right-click the villager to check trades and start automation."));
}
return;
}
// Step 3: Wait for player to check trades
if (currentState == AutomationState.SETUP_WAITING_FOR_INITIAL_CHECK) {
player.sendOverlayMessage(Component.literal("§e[Villager Roller] Step 3/3: Right-click the villager to check trades"));
if (stepCooldown > 0) {
stepCooldown--;
return;
}
if (player.containerMenu instanceof MerchantMenu menu && !menu.getOffers().isEmpty()) {
int matchIndex = TradeMatcher.findMatchingIndex(menu.getOffers(), targetOption, maxEmeraldPrice);
if (matchIndex != -1) {
currentState = AutomationState.SUCCESS;
matchedTradeIndex = matchIndex;
mousePositioned = false;
if (client.gameMode != null) {
client.gameMode.stopDestroyBlock();
}
releaseMovementKeys(client);
selectTradeAndPositionMouse(client, matchIndex);
mousePositioned = true;
playSound(SoundEvents.PLAYER_LEVELUP);
player.sendSystemMessage(Component.literal("§a§l[Villager Roller] SUCCESS! Desired trade already found!"));
} else {
attemptCount = 1;
playSound(SoundEvents.EXPERIENCE_ORB_PICKUP);
player.sendSystemMessage(Component.literal("§6[Villager Roller] §aInitial trade checked! Mod taking over automatically. Press X to stop."));
player.closeContainer();
this.originalPlayerPos = player.position();
currentState = AutomationState.AUTO_BREAKING;
tickCounter = 0;
}
}
return;
}
// Automatic Reroll Loop
if (targetVillager == null || workstationPos == null || client.level == null) {
handleError(player, "Villager or workstation lost!");
return;
}
if (player.distanceToSqr(Vec3.atCenterOf(workstationPos)) > 36 || player.distanceToSqr(targetVillager) > 49) {
handleError(player, "Moved too far away from workstation or villager!");
return;
}
tickCounter++;
switch (currentState) {
case AUTO_BREAKING -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Breaking workstation... (Attempt #" + attemptCount + ")"));
if (originalPlayerPos == null) {
this.originalPlayerPos = player.position();
}
// If player currently has 0 workstations in inventory, they only have the 1 placed
if (countWorkstationsInInventory(player) == 0) {
this.hasOnlyOneWorkstation = true;
}
// Switch to best tool in hotbar if available
BlockState state = client.level.getBlockState(workstationPos);
int bestSlot = findBestToolSlot(player, state);
if (bestSlot != -1 && bestSlot != player.getInventory().getSelectedSlot()) {
player.getInventory().setSelectedSlot(bestSlot);
}
if (client.gameMode != null) {
client.gameMode.startDestroyBlock(workstationPos, Direction.UP);
}
currentState = AutomationState.AUTO_WAITING_BREAK;
tickCounter = 0;
}
case AUTO_WAITING_BREAK -> {
if (client.level.getBlockState(workstationPos).isAir()) {
int currentInvCount = countWorkstationsInInventory(player);
if (hasOnlyOneWorkstation || currentInvCount == 0) {
this.hasOnlyOneWorkstation = true;
currentState = AutomationState.AUTO_RETRIEVING_WORKSTATION;
tickCounter = 0;
} else {
currentState = AutomationState.AUTO_WAITING_UNEMPLOY;
tickCounter = 0;
}
} else {
BlockState state = client.level.getBlockState(workstationPos);
int bestSlot = findBestToolSlot(player, state);
if (bestSlot != -1 && bestSlot != player.getInventory().getSelectedSlot()) {
player.getInventory().setSelectedSlot(bestSlot);
}
if (client.gameMode != null) {
client.gameMode.continueDestroyBlock(workstationPos, Direction.UP);
}
if (tickCounter > 120) {
if (client.gameMode != null) {
client.gameMode.stopDestroyBlock();
client.gameMode.startDestroyBlock(workstationPos, Direction.UP);
}
tickCounter = 0;
}
}
}
case AUTO_RETRIEVING_WORKSTATION -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Retrieving workstation drop... (Attempt #" + attemptCount + ")"));
if (countWorkstationsInInventory(player) > 0) {
releaseMovementKeys(client);
ensureWorkstationInHotbar(client, player);
if (originalPlayerPos != null && player.distanceToSqr(originalPlayerPos) > 0.04) {
currentState = AutomationState.AUTO_RETURNING_TO_STATION;
tickCounter = 0;
} else {
currentState = AutomationState.AUTO_WAITING_UNEMPLOY;
tickCounter = 0;
}
return;
}
Vec3 target = getWorkstationDropPos(client);
if (target != null) {
moveTowards(client, player, target);
}
if (tickCounter > 80) {
releaseMovementKeys(client);
if (countWorkstationsInInventory(player) == 0) {
handleError(player, "Failed to retrieve dropped workstation!");
return;
}
currentState = AutomationState.AUTO_RETURNING_TO_STATION;
tickCounter = 0;
}
}
case AUTO_RETURNING_TO_STATION -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Returning to station... (Attempt #" + attemptCount + ")"));
if (originalPlayerPos == null || player.distanceToSqr(originalPlayerPos) < 0.04 || tickCounter > 80) {
releaseMovementKeys(client);
ensureWorkstationInHotbar(client, player);
currentState = AutomationState.AUTO_WAITING_UNEMPLOY;
tickCounter = 0;
return;
}
moveTowards(client, player, originalPlayerPos);
}
case AUTO_WAITING_UNEMPLOY -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Waiting for unemployment... (Attempt #" + attemptCount + ")"));
if (tickCounter >= 12) {
currentState = AutomationState.AUTO_PLACING;
tickCounter = 0;
}
}
case AUTO_PLACING -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Placing workstation... (Attempt #" + attemptCount + ")"));
int slot = ensureWorkstationInHotbar(client, player);
if (slot != -1) {
player.getInventory().setSelectedSlot(slot);
} else {
handleError(player, "Workstation item missing from inventory!");
return;
}
if (client.gameMode != null) {
BlockPos supportPos = workstationPos.below();
BlockHitResult hit = (!client.level.getBlockState(supportPos).isAir())
? new BlockHitResult(Vec3.atCenterOf(supportPos), Direction.UP, supportPos, false)
: new BlockHitResult(Vec3.atCenterOf(workstationPos), Direction.UP, workstationPos, false);
client.gameMode.useItemOn(player, InteractionHand.MAIN_HAND, hit);
}
currentState = AutomationState.AUTO_WAITING_PLACE;
tickCounter = 0;
}
case AUTO_WAITING_PLACE -> {
if (!client.level.getBlockState(workstationPos).isAir()) {
currentState = AutomationState.AUTO_WAITING_CLAIM;
tickCounter = 0;
} else if (tickCounter > 30) {
int slot = ensureWorkstationInHotbar(client, player);
if (slot != -1) player.getInventory().setSelectedSlot(slot);
if (client.gameMode != null) {
BlockPos supportPos = workstationPos.below();
BlockHitResult hit = (!client.level.getBlockState(supportPos).isAir())
? new BlockHitResult(Vec3.atCenterOf(supportPos), Direction.UP, supportPos, false)
: new BlockHitResult(Vec3.atCenterOf(workstationPos), Direction.UP, workstationPos, false);
client.gameMode.useItemOn(player, InteractionHand.MAIN_HAND, hit);
}
tickCounter = 0;
}
}
case AUTO_WAITING_CLAIM -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Waiting for profession claim... (Attempt #" + attemptCount + ")"));
if (tickCounter >= 25) {
currentState = AutomationState.AUTO_INTERACTING;
tickCounter = 0;
}
}
case AUTO_INTERACTING -> {
player.sendOverlayMessage(Component.literal("§7[Villager Roller] Checking trade offers... (Attempt #" + attemptCount + ")"));
if (client.gameMode != null) {
client.gameMode.interact(player, targetVillager, new EntityHitResult(targetVillager), InteractionHand.MAIN_HAND);
}
currentState = AutomationState.AUTO_WAITING_OFFERS;
tickCounter = 0;
}
case AUTO_WAITING_OFFERS -> {
if (player.containerMenu instanceof MerchantMenu menu && !menu.getOffers().isEmpty()) {
int matchIndex = TradeMatcher.findMatchingIndex(menu.getOffers(), targetOption, maxEmeraldPrice);
if (matchIndex != -1) {
currentState = AutomationState.SUCCESS;
matchedTradeIndex = matchIndex;
mousePositioned = false;
if (client.gameMode != null) {
client.gameMode.stopDestroyBlock();
}
releaseMovementKeys(client);
// Open the trade screen now that the desired trade was found
client.gui.setScreen(new MerchantScreen(menu, player.getInventory(), Component.translatable("entity.minecraft.villager")));
selectTradeAndPositionMouse(client, matchIndex);
mousePositioned = true;
playSound(SoundEvents.PLAYER_LEVELUP);
player.sendSystemMessage(Component.literal("§a§l[Villager Roller] SUCCESS! Matching trade found after " + attemptCount + " rerolls!"));
} else {
attemptCount++;
player.sendOverlayMessage(Component.literal("§e[Villager Roller] Reroll #" + attemptCount + " - No match, continuing..."));
player.closeContainer();
currentState = AutomationState.AUTO_BREAKING;
tickCounter = 0;
}
} else if (tickCounter > 50) {
currentState = AutomationState.AUTO_INTERACTING;
tickCounter = 0;
}
}
default -> {}
}
}
private void selectTradeAndPositionMouse(Minecraft client, int tradeIndex) {
if (client.player == null) return;
// 1. Notify menu and send packet to server so trade is active
if (client.player.containerMenu instanceof MerchantMenu menu) {
menu.setSelectionHint(tradeIndex);
menu.tryMoveItems(tradeIndex);
if (client.getConnection() != null) {
client.getConnection().send(new ServerboundSelectTradePacket(tradeIndex));
}
}
// 2. If MerchantScreen is open, select trade in screen and position mouse over trade item
if (client.gui.screen() instanceof MerchantScreen screen) {
try {
Field shopItemField = MerchantScreen.class.getDeclaredField("shopItem");
shopItemField.setAccessible(true);
shopItemField.setInt(screen, tradeIndex);
int scrollOff = 0;
if (tradeIndex >= 7) {
scrollOff = tradeIndex - 6;
Field scrollOffField = MerchantScreen.class.getDeclaredField("scrollOff");
scrollOffField.setAccessible(true);
scrollOffField.setInt(screen, scrollOff);
}
Method postBtnMethod = MerchantScreen.class.getDeclaredMethod("postButtonClick");
postBtnMethod.setAccessible(true);
postBtnMethod.invoke(screen);
} catch (Throwable ignored) {}
int buttonIndex = tradeIndex;
if (buttonIndex >= 7) {
buttonIndex = 6;
}
int leftPos = (screen.width - 276) / 2;
int topPos = (screen.height - 166) / 2;
// In the trade offer entry on the left:
// Result item is drawn at leftPos + 73, button Y is topPos + 18 + buttonIndex * 20 + 2
int guiX = leftPos + 73 + 8;
int guiY = topPos + 18 + (buttonIndex * 20) + 2 + 8;
Window window = client.getWindow();
double screenX = (double) guiX * (double) window.getScreenWidth() / (double) window.getGuiScaledWidth();
double screenY = (double) guiY * (double) window.getScreenHeight() / (double) window.getGuiScaledHeight();
InputConstants.releaseMouse(window, screenX, screenY);
try {
Field xposField = MouseHandler.class.getDeclaredField("xpos");
xposField.setAccessible(true);
xposField.setDouble(client.mouseHandler, screenX);
Field yposField = MouseHandler.class.getDeclaredField("ypos");
yposField.setAccessible(true);
yposField.setDouble(client.mouseHandler, screenY);
} catch (Throwable ignored) {}
screen.mouseMoved(guiX, guiY);
}
}
private int findBestToolSlot(LocalPlayer player, BlockState state) {
if (state == null || state.isAir()) return -1;
int currentSlot = player.getInventory().getSelectedSlot();
int bestSlot = currentSlot;
float bestSpeed = calculateToolSpeed(player.getInventory().getSelectedItem(), state);
boolean bestDrops = player.getInventory().getSelectedItem().isCorrectToolForDrops(state);
for (int i = 0; i < 9; i++) {
ItemStack stack = player.getInventory().getItem(i);
if (!stack.isEmpty()) {
float speed = calculateToolSpeed(stack, state);
boolean drops = stack.isCorrectToolForDrops(state);
if (drops && !bestDrops) {
bestSpeed = speed;
bestDrops = true;
bestSlot = i;
} else if (drops == bestDrops && speed > bestSpeed) {
bestSpeed = speed;
bestSlot = i;
}
}
}
return bestSlot;
}
private float calculateToolSpeed(ItemStack stack, BlockState state) {
if (stack == null || stack.isEmpty()) return 1.0F;
float speed = stack.getDestroySpeed(state);
if (speed > 1.0F) {
ItemEnchantments enchants = stack.get(DataComponents.ENCHANTMENTS);
if (enchants != null) {
for (var entry : enchants.entrySet()) {
String regName = entry.getKey().getRegisteredName();
if (regName != null && regName.contains("efficiency")) {
int lvl = entry.getIntValue();
if (lvl > 0) {
speed += (lvl * lvl + 1);
}
break;
}
}
}
}
return speed;
}
private int findHotbarSlot(LocalPlayer player, Item item) {
if (item == null) return player.getInventory().getSelectedSlot();
if (player.getInventory().getSelectedItem().is(item)) {
return player.getInventory().getSelectedSlot();
}
for (int i = 0; i < 9; i++) {
if (player.getInventory().getItem(i).is(item)) {
return i;
}
}
return -1;
}
private void handleError(LocalPlayer player, String message) {
releaseMovementKeys(Minecraft.getInstance());
this.lastError = message;
this.currentState = AutomationState.ERROR;
player.sendSystemMessage(Component.literal("§c[Villager Roller] " + message));
}
private int countWorkstationsInInventory(LocalPlayer player) {
if (workstationItem == null || player == null) return 0;
int count = 0;
for (int i = 0; i < 36; i++) {
ItemStack stack = player.getInventory().getItem(i);
if (!stack.isEmpty() && stack.is(workstationItem)) {
count += stack.getCount();
}
}
if (player.getOffhandItem().is(workstationItem)) {
count += player.getOffhandItem().getCount();
}
return count;
}
private int ensureWorkstationInHotbar(Minecraft client, LocalPlayer player) {
if (workstationItem == null || player == null) return -1;
int hotbarSlot = findHotbarSlot(player, workstationItem);
if (hotbarSlot != -1) {
return hotbarSlot;
}
for (int i = 9; i < 36; i++) {
ItemStack stack = player.getInventory().getItem(i);
if (!stack.isEmpty() && stack.is(workstationItem)) {
int targetHotbar = player.getInventory().getSelectedSlot();
if (client.gameMode != null) {
client.gameMode.handleContainerInput(
player.inventoryMenu.containerId,
i,
targetHotbar,
ContainerInput.SWAP,
player
);
}
return targetHotbar;
}
}
return -1;
}
private Vec3 getWorkstationDropPos(Minecraft client) {
if (client.level != null && workstationPos != null && workstationItem != null) {
AABB searchBox = new AABB(workstationPos).inflate(3.5);
List<ItemEntity> items = client.level.getEntitiesOfClass(ItemEntity.class, searchBox,
e -> e.isAlive() && e.getItem().is(workstationItem));
if (!items.isEmpty()) {
return items.get(0).position();
}
}
return (workstationPos != null) ? Vec3.atCenterOf(workstationPos) : null;
}
private void moveTowards(Minecraft client, LocalPlayer player, Vec3 target) {
if (client.options == null || player == null || target == null) return;
double deltaX = target.x - player.getX();
double deltaZ = target.z - player.getZ();
double distSq = deltaX * deltaX + deltaZ * deltaZ;
if (distSq < 0.04) {
releaseMovementKeys(client);
return;
}
float yawRad = (float) Math.toRadians(player.getYRot());
double fx = -Math.sin(yawRad);
double fz = Math.cos(yawRad);
double rx = -Math.cos(yawRad);
double rz = -Math.sin(yawRad);
double forwardDot = deltaX * fx + deltaZ * fz;
double rightDot = deltaX * rx + deltaZ * rz;
client.options.keyUp.setDown(forwardDot > 0.15);
client.options.keyDown.setDown(forwardDot < -0.15);
client.options.keyRight.setDown(rightDot > 0.15);
client.options.keyLeft.setDown(rightDot < -0.15);
if (player.horizontalCollision) {
client.options.keyJump.setDown(true);
} else {
client.options.keyJump.setDown(false);
}
}
private void releaseMovementKeys(Minecraft client) {
if (client != null && client.options != null) {
client.options.keyUp.setDown(false);
client.options.keyDown.setDown(false);
client.options.keyLeft.setDown(false);
client.options.keyRight.setDown(false);
client.options.keyJump.setDown(false);
client.options.keyAttack.setDown(false);
client.options.keyUse.setDown(false);
}
}
private void playSound(net.minecraft.sounds.SoundEvent sound) {
Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(sound, 1.0F));
}
public boolean isSilentScreenSuppressed() {
return (currentState == AutomationState.AUTO_INTERACTING
|| currentState == AutomationState.AUTO_WAITING_OFFERS);
}
public AutomationState getState() { return currentState; }
public int getAttemptCount() { return attemptCount; }
public String getLastError() { return lastError; }
public TradeOption getTargetOption() { return targetOption; }
public int getMaxEmeraldPrice() { return maxEmeraldPrice; }
}
@@ -0,0 +1,10 @@
package com.villagerroller.config;
public class ModConfig {
public static int breakDelay = 20;
public static int placeDelay = 20;
public static int inspectDelay = 40;
public static boolean soundEnabled = true;
public static boolean silentTradeCheck = true;
public static boolean keepFocusedOnUnfocus = true;
}
@@ -0,0 +1,23 @@
package com.villagerroller.mixin;
import com.villagerroller.VillagerRoller;
import com.villagerroller.config.ModConfig;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Gui.class)
public class GuiMixin {
@Inject(method = "setScreen", at = @At("HEAD"), cancellable = true)
private void villagerroller$onSetScreen(Screen screen, CallbackInfo ci) {
if (ModConfig.silentTradeCheck && screen instanceof MerchantScreen) {
if (VillagerRoller.getStateMachine() != null && VillagerRoller.getStateMachine().isSilentScreenSuppressed()) {
ci.cancel();
}
}
}
}
@@ -0,0 +1,18 @@
package com.villagerroller.mixin;
import com.villagerroller.ui.RerollHudOverlay;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.Hud;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Hud.class)
public class HudMixin {
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void villagerroller$onExtractRenderState(GuiGraphicsExtractor extractor, DeltaTracker deltaTracker, CallbackInfo ci) {
RerollHudOverlay.render(extractor);
}
}
@@ -0,0 +1,18 @@
package com.villagerroller.mixin;
import com.mojang.blaze3d.platform.Window;
import com.villagerroller.config.ModConfig;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Window.class)
public class WindowMixin {
@Inject(method = "isFocused", at = @At("HEAD"), cancellable = true)
private void villagerroller$keepFocused(CallbackInfoReturnable<Boolean> cir) {
if (ModConfig.keepFocusedOnUnfocus) {
cir.setReturnValue(true);
}
}
}
@@ -0,0 +1,41 @@
package com.villagerroller.trade;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import java.util.ArrayList;
import java.util.List;
public class TradeCategory {
private final String name;
private final Item workstationItem;
private final Block workstationBlock;
private final List<TradeOption> options;
public TradeCategory(String name, Item workstationItem, Block workstationBlock) {
this.name = name;
this.workstationItem = workstationItem;
this.workstationBlock = workstationBlock;
this.options = new ArrayList<>();
}
public void addOption(TradeOption option) {
this.options.add(option);
}
public String getName() {
return name;
}
public Item getWorkstationItem() {
return workstationItem;
}
public Block getWorkstationBlock() {
return workstationBlock;
}
public List<TradeOption> getOptions() {
return options;
}
}
@@ -0,0 +1,140 @@
package com.villagerroller.trade;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.enchantment.ItemEnchantments;
import net.minecraft.world.item.trading.MerchantOffer;
public class TradeOption {
private final String displayName;
private final Item resultItem;
private final Item costItem;
private final int targetCostCount;
private final String enchantmentKey;
private final int minLevel;
// For Enchantment options
public TradeOption(String displayName, String enchantmentKey, int minLevel) {
this.displayName = displayName;
this.resultItem = Items.ENCHANTED_BOOK;
this.costItem = null;
this.targetCostCount = 64;
this.enchantmentKey = enchantmentKey;
this.minLevel = minLevel;
}
// For Item buying/selling options
public TradeOption(String displayName, Item resultItem, Item costItem, int targetCostCount) {
this.displayName = displayName;
this.resultItem = resultItem;
this.costItem = costItem;
this.targetCostCount = targetCostCount;
this.enchantmentKey = null;
this.minLevel = 0;
}
public String getDisplayName() {
return displayName;
}
public Item getResultItem() {
return resultItem;
}
public Item getCostItem() {
return costItem;
}
public int getTargetCostCount() {
return targetCostCount;
}
public String getEnchantmentKey() {
return enchantmentKey;
}
public int getMinLevel() {
return minLevel;
}
public ItemStack getDisplayStack() {
if (enchantmentKey != null) {
return new ItemStack(Items.ENCHANTED_BOOK);
}
if (costItem != null && resultItem == Items.EMERALD) {
return new ItemStack(costItem);
}
if (resultItem != null) {
return new ItemStack(resultItem);
}
return new ItemStack(Items.EMERALD);
}
public boolean matches(MerchantOffer offer, int maxEmeraldPrice) {
if (offer == null) return false;
// 1. Enchantment Matching
if (enchantmentKey != null) {
ItemStack result = offer.getResult();
if (!result.is(Items.ENCHANTED_BOOK)) return false;
if (!enchantmentKey.equalsIgnoreCase("any")) {
ItemEnchantments stored = result.get(DataComponents.STORED_ENCHANTMENTS);
if (stored == null) stored = result.get(DataComponents.ENCHANTMENTS);
if (stored == null || stored.isEmpty()) return false;
boolean found = false;
for (var entry : stored.entrySet()) {
String regName = entry.getKey().getRegisteredName();
int level = entry.getIntValue();
if (regName != null && regName.toLowerCase().contains(enchantmentKey.toLowerCase())) {
if (level >= minLevel) {
found = true;
break;
}
}
}
if (!found) return false;
}
// Check emerald cost
int emeraldCost = getEmeraldCost(offer);
return emeraldCost <= maxEmeraldPrice;
}
// 2. Selling resources for emeralds (e.g. 32 Sticks -> 1 Emerald)
if (costItem != null && resultItem == Items.EMERALD) {
ItemStack cost = offer.getCostA();
if (cost.is(costItem)) {
if (targetCostCount > 0 && cost.getCount() > targetCostCount) {
return false;
}
return offer.getResult().is(Items.EMERALD);
}
return false;
}
// 3. Buying items with emeralds (e.g. Emerald -> Arrows, Bread, Armor)
if (resultItem != null) {
if (offer.getResult().is(resultItem)) {
int emeraldCost = getEmeraldCost(offer);
return emeraldCost <= maxEmeraldPrice;
}
}
return false;
}
private int getEmeraldCost(MerchantOffer offer) {
int cost = 0;
if (offer.getCostA().is(Items.EMERALD)) {
cost += offer.getCostA().getCount();
}
if (offer.getCostB().is(Items.EMERALD)) {
cost += offer.getCostB().getCount();
}
return cost;
}
}
@@ -0,0 +1,142 @@
package com.villagerroller.trade;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TradeRegistry {
private static final List<TradeCategory> CATEGORIES = new ArrayList<>();
static {
// 1. Lectern (Librarian)
TradeCategory librarian = new TradeCategory("Lectern (Librarian)", Items.LECTERN, Blocks.LECTERN);
librarian.addOption(new TradeOption("Mending", "mending", 1));
librarian.addOption(new TradeOption("Unbreaking III", "unbreaking", 3));
librarian.addOption(new TradeOption("Efficiency V", "efficiency", 5));
librarian.addOption(new TradeOption("Sharpness V", "sharpness", 5));
librarian.addOption(new TradeOption("Fortune III", "fortune", 3));
librarian.addOption(new TradeOption("Silk Touch", "silk_touch", 1));
librarian.addOption(new TradeOption("Protection IV", "protection", 4));
librarian.addOption(new TradeOption("Feather Falling IV", "feather_falling", 4));
librarian.addOption(new TradeOption("Looting III", "looting", 3));
librarian.addOption(new TradeOption("Infinity", "infinity", 1));
librarian.addOption(new TradeOption("Power V", "power", 5));
librarian.addOption(new TradeOption("Flame", "flame", 1));
librarian.addOption(new TradeOption("Fire Aspect II", "fire_aspect", 2));
librarian.addOption(new TradeOption("Sweeping Edge III", "sweeping", 3));
librarian.addOption(new TradeOption("Thorns III", "thorns", 3));
librarian.addOption(new TradeOption("Depth Strider III", "depth_strider", 3));
librarian.addOption(new TradeOption("Frost Walker II", "frost_walker", 2));
librarian.addOption(new TradeOption("Respiration III", "respiration", 3));
librarian.addOption(new TradeOption("Aqua Affinity", "aqua_affinity", 1));
librarian.addOption(new TradeOption("Soul Speed III", "soul_speed", 3));
librarian.addOption(new TradeOption("Swift Sneak III", "swift_sneak", 3));
librarian.addOption(new TradeOption("Quick Charge III", "quick_charge", 3));
librarian.addOption(new TradeOption("Multishot", "multishot", 1));
librarian.addOption(new TradeOption("Piercing IV", "piercing", 4));
librarian.addOption(new TradeOption("Riptide III", "riptide", 3));
librarian.addOption(new TradeOption("Loyalty III", "loyalty", 3));
librarian.addOption(new TradeOption("Channeling", "channeling", 1));
librarian.addOption(new TradeOption("Breach IV", "breach", 4));
librarian.addOption(new TradeOption("Density V", "density", 5));
librarian.addOption(new TradeOption("Wind Burst III", "wind_burst", 3));
librarian.addOption(new TradeOption("Any Enchanted Book", "any", 1));
librarian.addOption(new TradeOption("24 Paper -> 1 Emerald", Items.EMERALD, Items.PAPER, 24));
librarian.addOption(new TradeOption("1 Emerald -> 1 Bookshelf", Items.BOOKSHELF, Items.EMERALD, 9));
CATEGORIES.add(librarian);
// 2. Fletching Table (Fletcher)
TradeCategory fletcher = new TradeCategory("Fletching Table (Fletcher)", Items.FLETCHING_TABLE, Blocks.FLETCHING_TABLE);
fletcher.addOption(new TradeOption("32 Sticks -> 1 Emerald", Items.EMERALD, Items.STICK, 32));
fletcher.addOption(new TradeOption("1 Emerald -> 16 Arrows", Items.ARROW, Items.EMERALD, 1));
fletcher.addOption(new TradeOption("26 Flint -> 1 Emerald", Items.EMERALD, Items.FLINT, 26));
CATEGORIES.add(fletcher);
// 3. Composter (Farmer)
TradeCategory farmer = new TradeCategory("Composter (Farmer)", Items.COMPOSTER, Blocks.COMPOSTER);
farmer.addOption(new TradeOption("20 Wheat -> 1 Emerald", Items.EMERALD, Items.WHEAT, 20));
farmer.addOption(new TradeOption("26 Potatoes -> 1 Emerald", Items.EMERALD, Items.POTATO, 26));
farmer.addOption(new TradeOption("22 Carrots -> 1 Emerald", Items.EMERALD, Items.CARROT, 22));
farmer.addOption(new TradeOption("15 Beetroots -> 1 Emerald", Items.EMERALD, Items.BEETROOT, 15));
farmer.addOption(new TradeOption("1 Emerald -> 6 Bread", Items.BREAD, Items.EMERALD, 1));
CATEGORIES.add(farmer);
// 4. Blast Furnace (Armorer)
TradeCategory armorer = new TradeCategory("Blast Furnace (Armorer)", Items.BLAST_FURNACE, Blocks.BLAST_FURNACE);
armorer.addOption(new TradeOption("15 Coal -> 1 Emerald", Items.EMERALD, Items.COAL, 15));
armorer.addOption(new TradeOption("5 Emeralds -> Iron Helmet", Items.IRON_HELMET, Items.EMERALD, 5));
armorer.addOption(new TradeOption("9 Emeralds -> Iron Chestplate", Items.IRON_CHESTPLATE, Items.EMERALD, 9));
armorer.addOption(new TradeOption("7 Emeralds -> Iron Leggings", Items.IRON_LEGGINGS, Items.EMERALD, 7));
armorer.addOption(new TradeOption("4 Emeralds -> Iron Boots", Items.IRON_BOOTS, Items.EMERALD, 4));
CATEGORIES.add(armorer);
// 5. Smithing Table (Toolsmith)
TradeCategory toolsmith = new TradeCategory("Smithing Table (Toolsmith)", Items.SMITHING_TABLE, Blocks.SMITHING_TABLE);
toolsmith.addOption(new TradeOption("15 Coal -> 1 Emerald", Items.EMERALD, Items.COAL, 15));
toolsmith.addOption(new TradeOption("1 Emerald -> Iron Axe", Items.IRON_AXE, Items.EMERALD, 1));
toolsmith.addOption(new TradeOption("1 Emerald -> Iron Shovel", Items.IRON_SHOVEL, Items.EMERALD, 1));
toolsmith.addOption(new TradeOption("1 Emerald -> Iron Pickaxe", Items.IRON_PICKAXE, Items.EMERALD, 1));
toolsmith.addOption(new TradeOption("1 Emerald -> Iron Hoe", Items.IRON_HOE, Items.EMERALD, 1));
CATEGORIES.add(toolsmith);
// 6. Grindstone (Weaponsmith)
TradeCategory weaponsmith = new TradeCategory("Grindstone (Weaponsmith)", Items.GRINDSTONE, Blocks.GRINDSTONE);
weaponsmith.addOption(new TradeOption("15 Coal -> 1 Emerald", Items.EMERALD, Items.COAL, 15));
weaponsmith.addOption(new TradeOption("3 Emeralds -> Iron Sword", Items.IRON_SWORD, Items.EMERALD, 3));
weaponsmith.addOption(new TradeOption("2 Emeralds -> Iron Axe", Items.IRON_AXE, Items.EMERALD, 2));
CATEGORIES.add(weaponsmith);
// 7. Smoker (Butcher)
TradeCategory butcher = new TradeCategory("Smoker (Butcher)", Items.SMOKER, Blocks.SMOKER);
butcher.addOption(new TradeOption("14 Raw Chicken -> 1 Emerald", Items.EMERALD, Items.CHICKEN, 14));
butcher.addOption(new TradeOption("7 Raw Porkchop -> 1 Emerald", Items.EMERALD, Items.PORKCHOP, 7));
butcher.addOption(new TradeOption("4 Raw Rabbit -> 1 Emerald", Items.EMERALD, Items.RABBIT, 4));
butcher.addOption(new TradeOption("1 Emerald -> 5 Cooked Chicken", Items.COOKED_CHICKEN, Items.EMERALD, 1));
CATEGORIES.add(butcher);
// 8. Brewing Stand (Cleric)
TradeCategory cleric = new TradeCategory("Brewing Stand (Cleric)", Items.BREWING_STAND, Blocks.BREWING_STAND);
cleric.addOption(new TradeOption("32 Rotten Flesh -> 1 Emerald", Items.EMERALD, Items.ROTTEN_FLESH, 32));
cleric.addOption(new TradeOption("1 Emerald -> 2 Redstone Dust", Items.REDSTONE, Items.EMERALD, 1));
CATEGORIES.add(cleric);
// 9. Barrel (Fisherman)
TradeCategory fisherman = new TradeCategory("Barrel (Fisherman)", Items.BARREL, Blocks.BARREL);
fisherman.addOption(new TradeOption("20 Raw Cod -> 1 Emerald", Items.EMERALD, Items.COD, 20));
fisherman.addOption(new TradeOption("15 Raw Salmon -> 1 Emerald", Items.EMERALD, Items.SALMON, 15));
fisherman.addOption(new TradeOption("6 Emeralds -> 1 Campfire", Items.CAMPFIRE, Items.EMERALD, 6));
CATEGORIES.add(fisherman);
// 10. Cartography Table (Cartographer)
TradeCategory cartographer = new TradeCategory("Cartography Table (Cartographer)", Items.CARTOGRAPHY_TABLE, Blocks.CARTOGRAPHY_TABLE);
cartographer.addOption(new TradeOption("24 Paper -> 1 Emerald", Items.EMERALD, Items.PAPER, 24));
cartographer.addOption(new TradeOption("7 Emeralds -> 1 Blank Map", Items.MAP, Items.EMERALD, 7));
CATEGORIES.add(cartographer);
// 11. Cauldron (Leatherworker)
TradeCategory leatherworker = new TradeCategory("Cauldron (Leatherworker)", Items.CAULDRON, Blocks.CAULDRON);
leatherworker.addOption(new TradeOption("6 Leather -> 1 Emerald", Items.EMERALD, Items.LEATHER, 6));
leatherworker.addOption(new TradeOption("7 Emeralds -> Leather Pants", Items.LEATHER_LEGGINGS, Items.EMERALD, 7));
leatherworker.addOption(new TradeOption("5 Emeralds -> Leather Tunic", Items.LEATHER_CHESTPLATE, Items.EMERALD, 5));
CATEGORIES.add(leatherworker);
// 12. Stonecutter (Mason)
TradeCategory mason = new TradeCategory("Stonecutter (Mason)", Items.STONECUTTER, Blocks.STONECUTTER);
mason.addOption(new TradeOption("10 Clay Balls -> 1 Emerald", Items.EMERALD, Items.CLAY_BALL, 10));
mason.addOption(new TradeOption("1 Emerald -> 10 Bricks", Items.BRICK, Items.EMERALD, 1));
CATEGORIES.add(mason);
// 13. Loom (Shepherd)
TradeCategory shepherd = new TradeCategory("Loom (Shepherd)", Items.LOOM, Blocks.LOOM);
shepherd.addOption(new TradeOption("18 White Wool -> 1 Emerald", Items.EMERALD, Items.WOOL.white(), 18));
shepherd.addOption(new TradeOption("2 Emeralds -> 1 Shears", Items.SHEARS, Items.EMERALD, 2));
CATEGORIES.add(shepherd);
}
public static List<TradeCategory> getCategories() {
return Collections.unmodifiableList(CATEGORIES);
}
}
@@ -0,0 +1,11 @@
package com.villagerroller.ui;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;
public class ModMenuIntegration implements ModMenuApi {
@Override
public ConfigScreenFactory<?> getModConfigScreenFactory() {
return RerollKeybindsScreen::new;
}
}
@@ -0,0 +1,138 @@
package com.villagerroller.ui;
import com.villagerroller.VillagerRoller;
import com.villagerroller.automation.RerollStateMachine;
import com.villagerroller.automation.RerollStateMachine.AutomationState;
import com.villagerroller.config.ModConfig;
import com.villagerroller.trade.TradeOption;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
public class RerollHudOverlay {
public static void render(GuiGraphicsExtractor extractor) {
Minecraft client = Minecraft.getInstance();
if (client == null || client.player == null) return;
// Respect F1 hide GUI
if (client.gui != null && client.gui.hud != null && client.gui.hud.isHidden()) {
return;
}
RerollStateMachine stateMachine = VillagerRoller.getStateMachine();
if (stateMachine == null) return;
AutomationState state = stateMachine.getState();
if (state == AutomationState.IDLE) return;
TradeOption target = stateMachine.getTargetOption();
if (target == null) return;
Font font = client.font;
if (font == null) return;
// 1. Determine status string and accent color
String statusText = getStatusText(state, stateMachine.getLastError());
int accentColor = getAccentColor(state);
int borderColor = (state == AutomationState.SUCCESS || state == AutomationState.ERROR) ? accentColor : 0x55FFFFFF;
// 2. Format details
String stopKeyName = VillagerRoller.stopKey != null ? VillagerRoller.stopKey.getTranslatedKeyMessage().getString() : "X";
String stopHint = "§8[" + stopKeyName + "] Stop";
String tradeTitle = "§eTarget: §f" + target.getDisplayName();
String costString;
if (target.getEnchantmentKey() != null) {
costString = "§7Max Cost: §a" + stateMachine.getMaxEmeraldPrice() + " §7Emeralds";
} else if (target.getCostItem() != null && target.getResultItem() == Items.EMERALD) {
costString = "§7Sell: §a" + target.getTargetCostCount() + "x §7for §a1 Emerald";
} else {
costString = "§7Max Cost: §a" + stateMachine.getMaxEmeraldPrice() + " §7Emeralds";
}
String statusLine = "§7Status: " + statusText;
String attemptsString = "§7Attempts: §f" + stateMachine.getAttemptCount();
String focusKeyName = VillagerRoller.toggleFocusKey != null ? VillagerRoller.toggleFocusKey.getTranslatedKeyMessage().getString() : "U";
String focusString = "§7Focus Mode: " + (ModConfig.keepFocusedOnUnfocus ? "§aON" : "§cOFF") + " §8[" + focusKeyName + "]";
// 3. Dynamic width calculation
int titleWidth = font.width("Villager Roller") + 16 + font.width(stopHint);
int tradeWidth = 26 + font.width(tradeTitle);
int costWidth = 26 + font.width(costString);
int statusWidth = font.width(statusLine);
int attemptsWidth = font.width(attemptsString);
int focusWidth = font.width(focusString);
int maxContentWidth = Math.max(160, Math.max(titleWidth, Math.max(tradeWidth, Math.max(costWidth, Math.max(statusWidth, Math.max(attemptsWidth, focusWidth))))));
int boxWidth = maxContentWidth + 16;
int boxHeight = 84;
int x = 8;
int y = 8;
// 4. Render Box Background & Borders
extractor.fill(x, y, x + boxWidth, y + boxHeight, 0xDD111116);
extractor.outline(x, y, boxWidth, boxHeight, borderColor);
extractor.fill(x, y, x + boxWidth, y + 2, accentColor);
// 5. Header row
extractor.text(font, "§6§lVillager Roller", x + 8, y + 6, 0xFFFFFFFF, true);
extractor.text(font, stopHint, x + boxWidth - 8 - font.width(stopHint), y + 6, 0xFFAAAAAA, true);
extractor.fill(x + 6, y + 17, x + boxWidth - 6, y + 18, 0x22FFFFFF);
// 6. Item slot & target trade details
int slotX = x + 7;
int slotY = y + 21;
extractor.fill(slotX, slotY, slotX + 20, slotY + 20, 0x60000000);
extractor.outline(slotX, slotY, 20, 20, 0x44FFFFFF);
ItemStack displayStack = target.getDisplayStack();
extractor.fakeItem(displayStack, slotX + 2, slotY + 2);
extractor.itemDecorations(font, displayStack, slotX + 2, slotY + 2);
extractor.text(font, tradeTitle, x + 32, y + 23, 0xFFFFFFFF, true);
extractor.text(font, costString, x + 32, y + 33, 0xFFDDDDDD, true);
// 7. Status, attempts, and focus mode lines
extractor.text(font, statusLine, x + 8, y + 46, 0xFFFFFFFF, true);
extractor.text(font, attemptsString, x + 8, y + 58, 0xFFDDDDDD, true);
extractor.text(font, focusString, x + 8, y + 70, 0xFFDDDDDD, true);
}
private static String getStatusText(AutomationState state, String lastError) {
return switch (state) {
case SETUP_WAITING_FOR_VILLAGER -> "§e[1/3] Right-click Villager";
case SETUP_WAITING_FOR_WORKSTATION_BREAK -> "§e[2/3] Hit Workstation";
case SETUP_WAITING_FOR_WORKSTATION_PLACE -> "§e[3/3] Place Workstation";
case SETUP_WAITING_FOR_INITIAL_CHECK -> "§eChecking Initial Trades...";
case AUTO_BREAKING, AUTO_WAITING_BREAK -> "§6Breaking Workstation...";
case AUTO_RETRIEVING_WORKSTATION -> "§bRetrieving Station Item...";
case AUTO_RETURNING_TO_STATION -> "§bReturning to Station...";
case AUTO_WAITING_UNEMPLOY -> "§eWaiting for Unemployment...";
case AUTO_PLACING, AUTO_WAITING_PLACE -> "§6Placing Workstation...";
case AUTO_WAITING_CLAIM -> "§eWaiting for Profession...";
case AUTO_INTERACTING -> "§6Opening Trades...";
case AUTO_WAITING_OFFERS -> "§6Evaluating Offers...";
case SUCCESS -> "§a§lTRADE FOUND!";
case ERROR -> "§c§lError: " + (lastError.isEmpty() ? "Unknown" : lastError);
default -> "§7Idle";
};
}
private static int getAccentColor(AutomationState state) {
if (state == AutomationState.SUCCESS) {
return 0xFF22C55E; // Emerald Green
} else if (state == AutomationState.ERROR) {
return 0xFFEF4444; // Bright Red
} else if (state.name().startsWith("SETUP")) {
return 0xFF3B82F6; // Blue
} else {
return 0xFFF59E0B; // Amber / Gold
}
}
}
@@ -0,0 +1,178 @@
package com.villagerroller.ui;
import com.mojang.blaze3d.platform.InputConstants;
import com.villagerroller.VillagerRoller;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.options.controls.KeyBindsScreen;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component;
public class RerollKeybindsScreen extends Screen {
private final Screen parent;
private KeyMapping selectedKey = null;
private static final String[] LABELS = {
"Open Mod Menu:",
"Emergency Stop:",
"Toggle Background Focus:"
};
public RerollKeybindsScreen(Screen parent) {
super(Component.literal("Villager Roller - Keybinds"));
this.parent = parent;
}
@Override
protected void init() {
this.clearWidgets();
int centerX = this.width / 2;
int startY = 60;
int rowHeight = 28;
KeyMapping[] keys = new KeyMapping[] {
VillagerRoller.openGuiKey,
VillagerRoller.stopKey,
VillagerRoller.toggleFocusKey
};
for (int i = 0; i < keys.length; i++) {
final KeyMapping key = keys[i];
int y = startY + i * rowHeight;
Component keyText;
if (selectedKey == key) {
keyText = Component.literal("§e> ??? <");
} else {
boolean hasConflict = false;
if (this.minecraft != null && this.minecraft.options != null && this.minecraft.options.keyMappings != null) {
for (KeyMapping other : this.minecraft.options.keyMappings) {
if (other != key && key.same(other)) {
hasConflict = true;
break;
}
}
}
if (hasConflict) {
keyText = Component.literal("§c" + key.getTranslatedKeyMessage().getString());
} else {
keyText = key.getTranslatedKeyMessage();
}
}
// Keybind rebind button
this.addRenderableWidget(Button.builder(keyText, btn -> {
selectedKey = key;
init();
}).bounds(centerX - 10, y, 130, 20).build());
// Reset button
Button resetBtn = Button.builder(Component.literal("Reset"), btn -> {
key.setKey(key.getDefaultKey());
KeyMapping.resetMapping();
if (this.minecraft != null && this.minecraft.options != null) {
this.minecraft.options.save();
}
init();
}).bounds(centerX + 125, y, 55, 20).build();
resetBtn.active = !key.isDefault();
this.addRenderableWidget(resetBtn);
}
// Button to open Minecraft's native Controls -> Key Binds screen
int bottomY = startY + keys.length * rowHeight + 20;
this.addRenderableWidget(Button.builder(Component.literal("Open Game Controls / Key Binds"), btn -> {
if (this.minecraft != null && this.minecraft.options != null) {
this.minecraft.setScreenAndShow(new KeyBindsScreen(this, this.minecraft.options));
}
}).bounds(centerX - 120, bottomY, 240, 20).build());
// Done button
this.addRenderableWidget(Button.builder(Component.literal("Done"), btn -> {
this.onClose();
}).bounds(centerX - 60, bottomY + 26, 120, 20).build());
}
@Override
public boolean keyPressed(KeyEvent event) {
if (selectedKey != null) {
if (event.isEscape()) {
selectedKey.setKey(InputConstants.UNKNOWN);
} else {
selectedKey.setKey(InputConstants.getKey(event));
}
selectedKey = null;
KeyMapping.resetMapping();
if (this.minecraft != null && this.minecraft.options != null) {
this.minecraft.options.save();
}
init();
return true;
}
if (event.isEscape()) {
this.onClose();
return true;
}
return super.keyPressed(event);
}
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (selectedKey != null) {
selectedKey.setKey(InputConstants.Type.MOUSE.getOrCreate(event.button()));
selectedKey = null;
KeyMapping.resetMapping();
if (this.minecraft != null && this.minecraft.options != null) {
this.minecraft.options.save();
}
init();
return true;
}
return super.mouseClicked(event, doubleClick);
}
@Override
public void onClose() {
if (this.minecraft != null) {
this.minecraft.setScreenAndShow(this.parent);
}
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) {
super.extractRenderState(graphics, mouseX, mouseY, delta);
int centerX = this.width / 2;
int startY = 60;
int rowHeight = 28;
// Title and Subtitle
graphics.centeredText(this.font, Component.literal("§l§nVillager Roller - Keybinds"), centerX, 18, 0xFFFFFFFF);
String hint = (selectedKey != null)
? "§ePress any key or mouse button to bind (ESC to unbind)"
: "§7Click a keybind to change it, or reset to default.";
graphics.centeredText(this.font, Component.literal(hint), centerX, 36, 0xFFDDDDDD);
// Panel background
int panelWidth = 380;
int panelHeight = LABELS.length * rowHeight + 14;
int panelX = centerX - panelWidth / 2;
int panelY = startY - 8;
graphics.fill(panelX, panelY, panelX + panelWidth, panelY + panelHeight, 0x55000000);
graphics.outline(panelX, panelY, panelWidth, panelHeight, 0x44FFFFFF);
// Row labels
for (int i = 0; i < LABELS.length; i++) {
int y = startY + i * rowHeight + 5;
graphics.text(this.font, LABELS[i], centerX - 180, y, 0xFFFFFFFF, true);
}
}
}
@@ -0,0 +1,179 @@
package com.villagerroller.ui;
import com.villagerroller.automation.RerollStateMachine;
import com.villagerroller.trade.TradeCategory;
import com.villagerroller.trade.TradeOption;
import com.villagerroller.trade.TradeRegistry;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import java.util.List;
public class RerollScreen extends Screen {
private final RerollStateMachine stateMachine;
private int selectedCategoryIndex = 0;
private int selectedOptionIndex = 0;
private int categoryPage = 0;
private int optionPage = 0;
private int maxEmeraldPrice = 64;
private static final int ITEMS_PER_PAGE = 6;
private static final int[] PRICE_STEPS = {64, 32, 20, 15, 10};
public RerollScreen(RerollStateMachine stateMachine) {
super(Component.literal("Villager Roller - Trade Selector"));
this.stateMachine = stateMachine;
}
@Override
protected void init() {
this.clearWidgets();
List<TradeCategory> categories = TradeRegistry.getCategories();
if (selectedCategoryIndex >= categories.size()) selectedCategoryIndex = 0;
TradeCategory activeCategory = categories.get(selectedCategoryIndex);
int leftColX = this.width / 2 - 205;
int rightColX = this.width / 2 + 5;
int startY = 48;
int rowHeight = 22;
// Keybinds button in top right
this.addRenderableWidget(Button.builder(Component.literal("⚙ Keybinds"), btn -> {
if (this.minecraft != null) {
this.minecraft.setScreenAndShow(new RerollKeybindsScreen(this));
}
}).bounds(this.width - 92, 8, 84, 20).build());
// 1. Workstation Category buttons (Left Column)
int totalCatPages = (categories.size() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
if (categoryPage >= totalCatPages) categoryPage = 0;
int catStart = categoryPage * ITEMS_PER_PAGE;
int catEnd = Math.min(catStart + ITEMS_PER_PAGE, categories.size());
for (int i = catStart; i < catEnd; i++) {
final int catIdx = i;
TradeCategory cat = categories.get(i);
boolean isSelected = (catIdx == selectedCategoryIndex);
String label = (isSelected ? "§a§l▶ " : "§f") + cat.getName();
this.addRenderableWidget(Button.builder(Component.literal(label), btn -> {
selectedCategoryIndex = catIdx;
selectedOptionIndex = 0;
optionPage = 0;
init();
}).bounds(leftColX, startY + (i - catStart) * rowHeight, 195, 20).build());
}
// Category Pagination
int catPageY = startY + ITEMS_PER_PAGE * rowHeight + 2;
this.addRenderableWidget(Button.builder(Component.literal("◀ Prev"), btn -> {
if (categoryPage > 0) {
categoryPage--;
init();
}
}).bounds(leftColX, catPageY, 95, 20).build());
this.addRenderableWidget(Button.builder(Component.literal("Next ▶"), btn -> {
if (categoryPage < totalCatPages - 1) {
categoryPage++;
init();
}
}).bounds(leftColX + 100, catPageY, 95, 20).build());
// 2. Trade Option buttons (Right Column)
List<TradeOption> options = activeCategory.getOptions();
int totalOptPages = Math.max(1, (options.size() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE);
if (optionPage >= totalOptPages) optionPage = 0;
int optStart = optionPage * ITEMS_PER_PAGE;
int optEnd = Math.min(optStart + ITEMS_PER_PAGE, options.size());
for (int i = optStart; i < optEnd; i++) {
final int optIdx = i;
TradeOption opt = options.get(i);
boolean isSelected = (optIdx == selectedOptionIndex);
String label = (isSelected ? "§6§l★ " : "§7") + opt.getDisplayName();
this.addRenderableWidget(Button.builder(Component.literal(label), btn -> {
selectedOptionIndex = optIdx;
init();
}).bounds(rightColX, startY + (i - optStart) * rowHeight, 195, 20).build());
}
// Option Pagination
int optPageY = startY + ITEMS_PER_PAGE * rowHeight + 2;
this.addRenderableWidget(Button.builder(Component.literal("◀ Prev"), btn -> {
if (optionPage > 0) {
optionPage--;
init();
}
}).bounds(rightColX, optPageY, 95, 20).build());
this.addRenderableWidget(Button.builder(Component.literal("Next ▶"), btn -> {
if (optionPage < totalOptPages - 1) {
optionPage++;
init();
}
}).bounds(rightColX + 100, optPageY, 95, 20).build());
// 3. Max Price cycle button
int controlsY = catPageY + 26;
String priceLabel = "Max Price: §a" + (maxEmeraldPrice >= 64 ? "Any (≤64)" : "≤" + maxEmeraldPrice) + " Emeralds §7⟳";
this.addRenderableWidget(Button.builder(Component.literal(priceLabel), btn -> {
cyclePrice();
init();
}).bounds(this.width / 2 - 100, controlsY, 200, 20).build());
// 4. Confirm and Cancel Buttons
int actionY = controlsY + 24;
this.addRenderableWidget(Button.builder(Component.literal("§a§lConfirm & Start Setup"), btn -> {
TradeOption selectedOption = (!options.isEmpty() && selectedOptionIndex < options.size())
? options.get(selectedOptionIndex)
: null;
if (selectedOption != null) {
stateMachine.startInteractiveSetup(activeCategory, selectedOption, maxEmeraldPrice);
this.onClose();
}
}).bounds(this.width / 2 - 145, actionY, 140, 20).build());
this.addRenderableWidget(Button.builder(Component.literal("Cancel"), btn -> {
this.onClose();
}).bounds(this.width / 2 + 5, actionY, 140, 20).build());
}
private void cyclePrice() {
for (int i = 0; i < PRICE_STEPS.length; i++) {
if (PRICE_STEPS[i] == maxEmeraldPrice) {
maxEmeraldPrice = PRICE_STEPS[(i + 1) % PRICE_STEPS.length];
return;
}
}
maxEmeraldPrice = 64;
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) {
super.extractRenderState(graphics, mouseX, mouseY, delta);
graphics.centeredText(this.font, Component.literal("§l§nVillager Roller - Trade Selector"), this.width / 2, 12, 0xFFFFFFFF);
int leftColX = this.width / 2 - 205;
int rightColX = this.width / 2 + 5;
graphics.centeredText(this.font, Component.literal("§e1. Workstation"), leftColX + 97, 34, 0xFFFFFF55);
graphics.centeredText(this.font, Component.literal("§e2. Desired Trade"), rightColX + 97, 34, 0xFFFFFF55);
List<TradeCategory> categories = TradeRegistry.getCategories();
if (selectedCategoryIndex < categories.size()) {
TradeCategory cat = categories.get(selectedCategoryIndex);
List<TradeOption> options = cat.getOptions();
if (selectedOptionIndex < options.size()) {
String summary = "§7Target: §f" + cat.getName() + " §7» §6" + options.get(selectedOptionIndex).getDisplayName();
graphics.centeredText(this.font, Component.literal(summary), this.width / 2, this.height - 18, 0xFFDDDDDD);
}
}
}
}
@@ -0,0 +1,21 @@
package com.villagerroller.util;
import com.villagerroller.trade.TradeOption;
import net.minecraft.world.item.trading.MerchantOffer;
import net.minecraft.world.item.trading.MerchantOffers;
public class TradeMatcher {
public static int findMatchingIndex(MerchantOffers offers, TradeOption target, int maxEmeraldPrice) {
if (offers == null || target == null) return -1;
for (int i = 0; i < offers.size(); i++) {
if (target.matches(offers.get(i), maxEmeraldPrice)) {
return i;
}
}
return -1;
}
public static boolean matches(MerchantOffers offers, TradeOption target, int maxEmeraldPrice) {
return findMatchingIndex(offers, target, maxEmeraldPrice) != -1;
}
}
@@ -0,0 +1,6 @@
{
"key.category.villagerroller.general": "Villager Roller",
"key.villagerroller.open": "Open Villager Roller Menu",
"key.villagerroller.stop": "Emergency Stop Rerolling",
"key.villagerroller.toggle_focus": "Toggle Background Focus Mode"
}
@@ -0,0 +1,6 @@
{
"key.category.villagerroller.general": "Villager Roller",
"key.villagerroller.open": "Open Villager Roller Menu",
"key.villagerroller.stop": "Emergency Stop Rerolling",
"key.villagerroller.toggle_focus": "Toggle Background Focus Mode"
}
+29
View File
@@ -0,0 +1,29 @@
{
"schemaVersion": 1,
"id": "villager-roller",
"version": "${version}",
"name": "Villager Roller",
"description": "Automatically rerolls villager trades client-side.",
"authors": [
"jadonkortislolz-wq"
],
"contact": {},
"license": "MIT",
"environment": "client",
"entrypoints": {
"client": [
"com.villagerroller.VillagerRoller"
],
"modmenu": [
"com.villagerroller.ui.ModMenuIntegration"
]
},
"mixins": [
"villager-roller.mixins.json"
],
"depends": {
"fabricloader": ">=0.19.5",
"minecraft": "~26.3",
"java": ">=25"
}
}
@@ -0,0 +1,14 @@
{
"required": true,
"minVersion": "0.8",
"package": "com.villagerroller.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"GuiMixin",
"WindowMixin",
"HudMixin"
],
"injectors": {
"defaultRequire": 1
}
}