diff --git a/build.gradle.kts b/build.gradle.kts index f35dda499..833c3f5bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java") - id("fabric-loom").version("1.7-SNAPSHOT").apply(false) + id("fabric-loom").version("1.15-SNAPSHOT").apply(false) } subprojects { @@ -22,6 +22,8 @@ val mod_version: String by project val buildAll = tasks.create("buildAll") { dependsOn(":v1_21_4:build") + dependsOn(":v1_21_11:build") + // ... // This isn't working.... you still have to run each build individually /* tasks.findByName(":v1_19_3:build")?.mustRunAfter(":v1_19_4:build") tasks.findByName(":v1_19:build")?.mustRunAfter(":v1_19_3:build") diff --git a/gradle.properties b/gradle.properties index d12f32c8c..5ce0bdc54 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,3 +5,4 @@ org.gradle.parallel=false mod_version=3.4.0 maven_group=net.fabricmc archives_base_name=litematica-printer +org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.11.10-hotspot \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2617362fd..9937dae91 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip -networkTimeout=10000 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index c88d72cf2..73d2f0c47 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,3 +9,6 @@ pluginManagement { } } include 'v1_21_4' + +include(":v1_21_11") +project(":v1_21_11").projectDir = file("v1_21_11") diff --git a/v1_21_11/build.gradle.kts b/v1_21_11/build.gradle.kts new file mode 100644 index 000000000..bd1679c5d --- /dev/null +++ b/v1_21_11/build.gradle.kts @@ -0,0 +1,68 @@ +import net.fabricmc.loom.task.RemapJarTask + +plugins { + id("maven-publish") +} + +java.sourceCompatibility = JavaVersion.VERSION_21 +java.targetCompatibility = JavaVersion.VERSION_21 + +val archives_base_name: String by project + +//val malilib_version: String by project +//val litematica_projectid: String by project +//val litematica_fileid: String by project + +val mod_version: String by project +val minecraft_version: String by project +val yarn_mappings: String by project +val loader_version: String by project +val fabric_version: String by project + +dependencies { +// implementation(project(":common")) + minecraft("com.mojang:minecraft:${minecraft_version}") + mappings("net.fabricmc:yarn:${yarn_mappings}:v2") + annotationProcessor("io.github.llamalad7:mixinextras-fabric:0.4.1")?.let { implementation(it)?.let { include(it) } } + modImplementation("net.fabricmc:fabric-loader:${loader_version}") + modImplementation("net.fabricmc.fabric-api:fabric-api:${fabric_version}") +modImplementation("com.github.sakura-ryoko:malilib:1.21.11-0.27.8") + //Replace masa litematica with sakura-ryoko fork + modImplementation("com.github.sakura-ryoko:litematica:1.21.11-0.26.3") { + exclude(group = "fi.dy.masa.malilib") +} + modImplementation("com.ptsmods:devlogin:3.5") +} + +repositories { + maven("https://masa.dy.fi/maven") + maven("https://www.cursemaven.com") + maven("https://jitpack.io") + maven("https://maven.fallenbreath.me/releases") +} + +// Process resources +tasks.withType { + inputs.property("version", mod_version) + + filesMatching("fabric.mod.json") { + expand(mapOf("version" to mod_version)) + } +} + +tasks.build { + finalizedBy("renameJar") +} + +tasks.create("renameJar") { + val remapJar = tasks.getByName("remapJar") + val jarFile = remapJar.archiveFile.get().asFile + + doLast { + val targetFile = File(jarFile.parent, "$archives_base_name-$mod_version-mc$minecraft_version.jar") + println("Renaming ${jarFile.absolutePath} to ${targetFile.absolutePath}") + targetFile.delete() + jarFile.renameTo(targetFile) + } +} + diff --git a/v1_21_11/gradle.properties b/v1_21_11/gradle.properties new file mode 100644 index 000000000..16b3385dd --- /dev/null +++ b/v1_21_11/gradle.properties @@ -0,0 +1,14 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +# https://masa.dy.fi/maven/fi/dy/masa/malilib/ +#malilib_version=1.20.4:0.18.1 +# https://www.curseforge.com/minecraft/mc-mods/litematica/files +#litematica_fileid=5170070 +#litematica_projectid=308892 +# Fabric Properties: https://fabricmc.net/develop/ +minecraft_version=1.21.11 +yarn_mappings=1.21.11+build.4 +loader_version=0.18.1 + +# Fabric API +fabric_version=0.139.5+1.21.11 diff --git a/v1_21_11/gradle/wrapper/gradle-wrapper.jar b/v1_21_11/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..ccebba771 Binary files /dev/null and b/v1_21_11/gradle/wrapper/gradle-wrapper.jar differ diff --git a/v1_21_11/gradle/wrapper/gradle-wrapper.properties b/v1_21_11/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..0d1842103 --- /dev/null +++ b/v1_21_11/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/v1_21_11/gradlew b/v1_21_11/gradlew new file mode 100644 index 000000000..79a61d421 --- /dev/null +++ b/v1_21_11/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 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. +# + +############################################################################## +# +# Gradle 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 Gradle +# +# 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/HEAD/subprojects/plugins/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##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# 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"' + +# 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 + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# 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 + which java >/dev/null 2>&1 || 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 + +# 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=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=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" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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" "$@" diff --git a/v1_21_11/gradlew.bat b/v1_21_11/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/v1_21_11/gradlew.bat @@ -0,0 +1,92 @@ +@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 + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +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 + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/ActionHandler.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/ActionHandler.java new file mode 100644 index 000000000..f172347b8 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/ActionHandler.java @@ -0,0 +1,93 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import me.aleksilassila.litematica.printer.v1_21_4.actions.Action; +import me.aleksilassila.litematica.printer.v1_21_4.actions.ActionChain; +import me.aleksilassila.litematica.printer.v1_21_4.actions.PrepareAction; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; + +import java.util.LinkedList; +import java.util.Queue; + +public class ActionHandler { + private final MinecraftClient client; + private final ClientPlayerEntity player; + + private final Queue currentTickActions = new LinkedList<>(); + private final Queue previousTickActions = new LinkedList<>(); + public PrepareAction lookAction = null; + + public ActionHandler(MinecraftClient client, ClientPlayerEntity player) { + this.client = client; + this.player = player; + } + + public void processCurrentTickActions() { + Action nextAction = currentTickActions.poll(); + while (nextAction != null) { + if (LitematicaMixinMod.DEBUG) { + System.out.println("Sending action " + nextAction); + } + boolean success = nextAction.send(client, player); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + if (success) { + System.out.println("Action (Pre) success " + nextAction); + } else { + System.out.println("Action (Pre) failed " + nextAction); + } + } + if (!success) { + currentTickActions.clear(); + break; + } + nextAction = currentTickActions.poll(); + } + Printer.inactivityCounter = 0; + } + + public void processPreviousTickActions() { + // Hours spent figuring this out: 6 + Action nextAction = previousTickActions.poll(); + while (nextAction != null) { + if (LitematicaMixinMod.DEBUG) { + System.out.println("Sending action " + nextAction); + } + boolean success = nextAction.send(client, player); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + if (success) { + System.out.println("Action (Post Tick) success " + nextAction); + } else { + System.out.println("Action (Post Tick) failed " + nextAction); + } + } + if (!success) { + previousTickActions.clear(); + break; + } + nextAction = previousTickActions.poll(); + } + } + + public boolean acceptsActions() { + return currentTickActions.isEmpty() && previousTickActions.isEmpty(); + } + + public void addActions(Action... actions) { + if (!acceptsActions()) return; + + for (Action action : actions) { + if (action instanceof PrepareAction) + lookAction = (PrepareAction) action; + } + + for (Action action : actions) { + if (action instanceof ActionChain chain) { + currentTickActions.addAll(chain.getActionsCurrentTick()); + previousTickActions.addAll(chain.getActionsNextTick()); + } else { + currentTickActions.add(action); + } + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/BlockHelper.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/BlockHelper.java new file mode 100644 index 000000000..536e288fa --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/BlockHelper.java @@ -0,0 +1,31 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import net.minecraft.block.*; +import net.minecraft.item.Item; +import net.minecraft.item.Items; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +abstract public class BlockHelper { + public static List> interactiveBlocks = new ArrayList<>(Arrays.asList( + AbstractChestBlock.class, AbstractFurnaceBlock.class, CraftingTableBlock.class, LeverBlock.class, + DoorBlock.class, TrapdoorBlock.class, BedBlock.class, RedstoneWireBlock.class, ScaffoldingBlock.class, + HopperBlock.class, EnchantingTableBlock.class, NoteBlock.class, JukeboxBlock.class, CakeBlock.class, + FenceGateBlock.class, BrewingStandBlock.class, DragonEggBlock.class, CommandBlock.class, + BeaconBlock.class, AnvilBlock.class, ComparatorBlock.class, RepeaterBlock.class, + DropperBlock.class, DispenserBlock.class, ShulkerBoxBlock.class, LecternBlock.class, + FlowerPotBlock.class, BarrelBlock.class, BellBlock.class, SmithingTableBlock.class, + LoomBlock.class, CartographyTableBlock.class, GrindstoneBlock.class, + StonecutterBlock.class, AbstractSignBlock.class, AbstractCandleBlock.class)); + + public static final Item[] SHOVEL_ITEMS = new Item[]{ + Items.NETHERITE_SHOVEL, + Items.DIAMOND_SHOVEL, + Items.GOLDEN_SHOVEL, + Items.IRON_SHOVEL, + Items.STONE_SHOVEL, + Items.WOODEN_SHOVEL + }; +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/FreeLook.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/FreeLook.java new file mode 100644 index 000000000..8359c1632 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/FreeLook.java @@ -0,0 +1,109 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import fi.dy.masa.malilib.config.options.ConfigBoolean; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.Perspective; + +public class FreeLook { + static FreeLook INSTANCE = null; + float cameraYaw = 0; + float cameraPitch = 0; + Perspective prevPerspective = Perspective.FIRST_PERSON; + MinecraftClient mc = MinecraftClient.getInstance(); + boolean enabled = false; + int ticksSinceLastRotation = 0; + +// BooleanSetting changePers = addBooleanSetting("Change Perspective",true); +// EnumSetting cameraMode = addEnumSetting("Camera Mode",Mode.CAMERA); +// FloatSetting sensitivity = addFloatSetting("Sensitivity",8,0,10); + private FreeLook() { + enabled = PrinterConfig.FREE_LOOK.getBooleanValue(); + PrinterConfig.FREE_LOOK.setValueChangeCallback(this::setEnabled); + } + + public static FreeLook getInstance() { + if (INSTANCE == null) { + INSTANCE = new FreeLook(); + } + return INSTANCE; + } + + public void onGameTick() { + if (shouldRotate() && ((ticksSinceLastRotation -1) == PrinterConfig.FREE_LOOK_LOOK_BACK.getIntegerValue() || PrinterConfig.FREE_LOOK_LOOK_BACK_ALWAYS_ROTATE_PLAYER.getBooleanValue())) { + if (mc.player != null) { + // Reset player rotation. The mouse mixin only rotates the player by a delta. So without this the player + // rotation would be offset from the camera rotation. + mc.player.setYaw(cameraYaw); + mc.player.setPitch(cameraPitch); + } + } + ticksSinceLastRotation++; + } + + private void setEnabled(ConfigBoolean configBoolean) { + this.enabled = configBoolean.getBooleanValue(); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) System.out.println("FreeLook: " + enabled); + if (enabled) { + onEnable(); + } else { + onDisable(); + } + } + + void onEnable() { + if(mc.player == null) { + return; + } + this.enabled = true; + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) System.out.println("FreeLook: onEnable"); + + cameraPitch = mc.player.getPitch(); + cameraYaw = mc.player.getYaw(); + prevPerspective = mc.options.getPerspective(); + + if (PrinterConfig.FREE_LOOK_THIRD_PERSON.getBooleanValue()) { + mc.options.setPerspective(Perspective.THIRD_PERSON_BACK); + } else { + mc.options.setPerspective(Perspective.FIRST_PERSON); + } + } + + void onDisable() { + this.enabled = false; + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) System.out.println("FreeLook: onDisable"); + if (prevPerspective != null && mc.options.getPerspective() != prevPerspective /*&& changePers.getValue()*/) mc.options.setPerspective(prevPerspective); + } + + public boolean isEnabled() { + return enabled; + } + + public boolean shouldRotate() { + return enabled && PrinterConfig.FREE_LOOK_LOOK_BACK.getIntegerValue() != 0 && ticksSinceLastRotation > PrinterConfig.FREE_LOOK_LOOK_BACK.getIntegerValue(); + } + + public float getCameraYaw() { + return cameraYaw; + } + + public float getCameraPitch() { + return cameraPitch; + } + + public void setCameraYaw(float v) { + cameraYaw = v; + } + + public void setCameraPitch(float v) { + cameraPitch = v; + } + + public void setPrevPerspective(Perspective perspective) { + prevPerspective = perspective; + } + + public Perspective getPrevPerspective() { + return prevPerspective; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java new file mode 100644 index 000000000..a9a227786 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java @@ -0,0 +1,385 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import fi.dy.masa.litematica.config.Configs; +import fi.dy.masa.litematica.materials.MaterialCache; +import fi.dy.masa.litematica.util.InventoryUtils; +import fi.dy.masa.litematica.world.WorldSchematic; +import fi.dy.masa.malilib.config.options.ConfigString; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateSelectedSlotC2SPacket; +import net.minecraft.screen.PlayerScreenHandler; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.util.Hand; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class InventoryManager { + private static final List USABLE_SLOTS = new ArrayList<>(); + int delay = 0; + /** + * The queue of items to pull from the inventory + */ + private final ArrayList hotbarSlots = new ArrayList<>(9); + + private final MinecraftClient mc = MinecraftClient.getInstance(); + private final Deque rollingSlots = new ArrayDeque<>(); + private final Deque lastUsedSlots = new ArrayDeque<>(); + private static InventoryManager instance; + + private InventoryManager() { + // Probably add a way to configure this + for (int i = 0; i < 9; i++) { + rollingSlots.add(i); + lastUsedSlots.add(i); + hotbarSlots.add(new SlotInfo()); + } + } + + public static void setHotbarSlots(ConfigString config) { + USABLE_SLOTS.clear(); + String configStr = config.getStringValue(); + String[] parts = configStr.split(","); + + for (String str : parts) { + try { + int slotNum = Integer.parseInt(str) - 1; + + if (PlayerInventory.isValidHotbarIndex(slotNum) && + !USABLE_SLOTS.contains(slotNum)) { + USABLE_SLOTS.add(slotNum); + } + } catch (NumberFormatException ignore) { + } + } + } + + public static InventoryManager getInstance() { + if (instance == null) { + instance = new InventoryManager(); + } + return instance; + } + + public void reset() { + hotbarSlots.forEach((info) -> info.ticksLocked = 0); + } + + public boolean tick() { + if (mc.player == null || mc.interactionManager == null) { + return false; + } + + delay = Math.max(0, delay - 1); + + return false; + } + + /** + * Swaps the item from the inventory to the hotbar + * + * @param item The item to pull from the inventory + * @return True if the item could the swapped into the hotbar + */ + private boolean swapToHotbar(ClientPlayerEntity player, Item item) { + if (getHotbarSlotWithItem(player, new ItemStack(item)) != -1) { // Already in the hotbar dummy + return true; + } + int slot = getBestInventorySlotWithItem(player, new ItemStack(item)); + if (slot == -1) { + return false; + } + int nextSlot = nextHotbarSlot(); + if (nextSlot == -1) { + return false; + } + hotbarSlots.get(nextSlot).addTicksLocked(10); + hotbarSlots.get(nextSlot).waitingForItem = item; + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(nextSlot); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + System.out.println("Swapping item from inventory: " + slot + " into hotbar -> " + nextSlot); + } + swapAway(slot, nextSlot, player.playerScreenHandler); + delay += PrinterConfig.INVENTORY_DELAY.getIntegerValue(); + return true; + } + + public void pickSlot(WorldSchematic world, ClientPlayerEntity player, BlockPos pos) { + if (mc.interactionManager == null) { + return; + } + PlayerInventory inv = player.getInventory(); + BlockState state = world.getBlockState(pos); + ItemStack stack = MaterialCache.getInstance().getRequiredBuildItemForState(state, world, pos); + int slot = inv.getSlotWithStack(stack); + boolean shouldPick = slot > 8; + if (slot != -1 && !shouldPick) { + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(slot); + } else if (slot != -1) { + InventoryUtils.setPickedItemToHand(slot, stack, mc); // https://github.com/sakura-ryoko/litematica-printer/blob/f8e38a2b31708e61f8a5fad0f2989d6834495da4/src/main/java/me/aleksilassila/litematica/printer/actions/PrepareAction.java#L71 + } else if (Configs.Generic.PICK_BLOCK_SHULKERS.getBooleanValue()) { + slot = findSlotWithBoxWithItem(player.getInventory(), stack, true); + if (slot > -1) { + if (slot > 8) { + InventoryUtils.setPickedItemToHand(slot, stack, mc); + } else { + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) inv).setSelectedSlot(slot); + } + } + } + } + + private void depositCursorStack() { + mc.getNetworkHandler().sendPacket(new CloseHandledScreenC2SPacket(mc.player.currentScreenHandler.syncId)); +// if (!mc.player.playerScreenHandler.getCursorStack().isEmpty()) { +// mc.player.getInventory().offerOrDrop(mc.player.playerScreenHandler.getCursorStack()); +// mc.player.playerScreenHandler.setCursorStack(ItemStack.EMPTY); +// } + } + + public static int findSlotWithBoxWithItem(PlayerInventory inventory, ItemStack stackReference, boolean lestFirst) { + int bestCount = lestFirst ? Integer.MAX_VALUE : 0; + int bestSlot = -1; + + for (int slotNum = 0; slotNum < 36; slotNum += 1) { + ItemStack itemStack = inventory.getStack(slotNum); + int count = shulkerBoxItemCount(itemStack, stackReference); + if (lestFirst && count < bestCount && count > 0) { + bestCount = count; + bestSlot = slotNum; + } else if (!lestFirst && count > bestCount) { + bestCount = count; + bestSlot = slotNum; + } + } + + return bestSlot; + } + + public static int shulkerBoxItemCount(ItemStack stack, ItemStack referenceItem) { + DefaultedList items = fi.dy.masa.malilib.util.InventoryUtils.getStoredItems(stack); + int count = 0; + if (!items.isEmpty()) { + for (ItemStack item : items) { + if (fi.dy.masa.malilib.util.InventoryUtils.areStacksEqual(item, referenceItem)) { + count += item.getCount(); + } + } + } + + return count; + } + + public boolean select(ItemStack itemStack) { + ClientPlayerEntity player = mc.player; + if (player == null || mc.interactionManager == null) { + return false; + } + + // Swap logic start + if (itemStack != null) { + PlayerInventory inventory = player.getInventory(); + + // This thing is straight from MinecraftClient#doItemPick() + if (player.getAbilities().creativeMode) { + depositCursorStack(); + this.addPickBlock(inventory, itemStack); + mc.interactionManager.clickCreativeStack(player.getStackInHand(Hand.MAIN_HAND), 36 + inventory.getSelectedSlot()); + updateLastUsedSlot(inventory.getSelectedSlot()); + return true; + } else { + int hotbarSlot = getHotbarSlotWithItem(player, itemStack); + if (hotbarSlot == -1) { + // Start equipping from inventory + if (delay > 0) { + return false; + } + depositCursorStack(); + if (swapToHotbar(mc.player, itemStack.getItem())) { + // If true the item should now be somewhere in the hotbar + hotbarSlot = getHotbarSlotWithItem(player, itemStack); + if (hotbarSlot == -1) { + return false; + } + if (hotbarSlot != player.getInventory().getSelectedSlot()) { + player.networkHandler.sendPacket(new UpdateSelectedSlotC2SPacket(hotbarSlot)); + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(hotbarSlot); + } + updateLastUsedSlot(hotbarSlot); + return false; + } + return false; + } else { + depositCursorStack(); + // Switch to hotbar slot + if (hotbarSlot != player.getInventory().getSelectedSlot()) { + player.networkHandler.sendPacket(new UpdateSelectedSlotC2SPacket(hotbarSlot)); + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(hotbarSlot); + } + updateLastUsedSlot(hotbarSlot); + return true; + } + } + } + return false; + } + + // https://github.com/sakura-ryoko/litematica-printer/blob/f8e38a2b31708e61f8a5fad0f2989d6834495da4/src/main/java/me/aleksilassila/litematica/printer/actions/PrepareAction.java#L95 + private void addPickBlock(PlayerInventory inv, ItemStack stack) { + me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor invAccessor = + (me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) inv; + int slot = inv.getSlotWithStack(stack); + + if (slot >= 0 && slot < 9) { + invAccessor.setSelectedSlot(slot); + } else { + if (slot == -1) { + invAccessor.setSelectedSlot(inv.getSwappableHotbarSlot()); + + if (!inv.getStack(inv.getSelectedSlot()).isEmpty()) { + int empty = inv.getEmptySlot(); + + if (empty != -1) { + inv.setStack(empty, inv.getStack(inv.getSelectedSlot())); + } + } + inv.setStack(inv.getSelectedSlot(), stack); + } else { + inv.swapSlotWithHotbar(slot); + } + } + } + + public boolean swapAway(int inventorySlot, int hotbarSlot, PlayerScreenHandler screenHandler) { + ClientPlayNetworkHandler networkHandler = mc.getNetworkHandler(); + if (mc.player == null || mc.interactionManager == null || networkHandler == null) return false; + + mc.interactionManager.clickSlot(screenHandler.syncId, inventorySlot, hotbarSlot, SlotActionType.SWAP, mc.player); + return true; + } + + /** + * Returns the next available slot for a new item. Returns the slot to the end of the queue after returning. + * Range from 0-8 + * + * @return The next available slot + */ + private int nextHotbarSlot() { + final String mode = PrinterConfig.PRINTER_INVENTORY_MANAGEMENT_MODE.getStringValue(); + + if (PrinterConfig.InventoryManagementModeEnum.LEAST_USED.is(mode)) { + if (!lastUsedSlots.isEmpty()) { + List usableSlots = lastUsedSlots.stream().filter(USABLE_SLOTS::contains).toList(); + if (usableSlots.isEmpty()) { + return -1; + } + int last = usableSlots.getLast(); + lastUsedSlots.remove(last); + lastUsedSlots.addFirst(last); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + System.out.println("Next new least used slot: " + last + "Last used slots: " + lastUsedSlots); + } + return last; + } + } else if (PrinterConfig.InventoryManagementModeEnum.ROLLING.is(mode)) { + if (!rollingSlots.isEmpty()) { + List usableSlots = rollingSlots.stream().filter(USABLE_SLOTS::contains).toList(); + if (usableSlots.isEmpty()) { + return -1; + } + int slot = usableSlots.getLast(); + rollingSlots.remove(slot); + rollingSlots.addLast(slot); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + System.out.println("Next new rolling slot: " + slot + "Rolling slots: " + rollingSlots); + } + return slot; + } + } + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + System.out.println("No slots available"); + } + return -1; + } + + /** + * Returns the first slot with the given item. Returns -1 if no slot is found. + * + * @param player The player to check + * @param itemStack The item to check for + * @return The first slot with the given item + */ + private static int getBestInventorySlotWithItem(ClientPlayerEntity player, ItemStack itemStack) { + PlayerInventory inventory = player.getInventory(); + + if (itemStack.isEmpty()) return -1; + + int lowestCount = 0; + int lowestSlot = -1; + for (int i = 9; i < 36; ++i) { + if (!inventory.getStack(i).isEmpty() && ItemStack.areItemsAndComponentsEqual(itemStack, inventory.getStack(i))) { + if (inventory.getStack(i).getCount() < lowestCount || lowestSlot == -1) { + lowestCount = inventory.getStack(i).getCount(); + lowestSlot = i; + } + } + } + + return lowestSlot; + } + + /** + * Return a number between 0-8 representing the hotbar slot with the given item. + */ + public int getHotbarSlotWithItem(ClientPlayerEntity player, ItemStack itemStack) { + PlayerInventory inventory = player.getInventory(); + + if (itemStack.isEmpty()) return -1; + + for (int i = 0; i < 9; ++i) { + if (!inventory.getStack(i).isEmpty() && ItemStack.areItemsEqual(inventory.getStack(i), itemStack)) { +// if (hotbarSlots.get(i).ticksLocked == 0) { + return i; +// } + } + } + + return -1; + } + + private void updateLastUsedSlot(int slot) { + lastUsedSlots.remove(slot); + lastUsedSlots.addFirst(slot); + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { + String list = lastUsedSlots.stream().map(String::valueOf).reduce((a, b) -> a + ", " + b).orElse(""); + System.out.println("Updating last used slot: " + slot + ". Now: " + list); + } + } + + static class SlotInfo { + private int ticksLocked = 0; + @Nullable + public Item waitingForItem = null; + + public void addTicksLocked(int ticks) { + ticksLocked += ticks; + } + + public void decrementTicksLocked() { + ticksLocked--; + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/LitematicaMixinMod.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/LitematicaMixinMod.java new file mode 100644 index 000000000..18f6e0244 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/LitematicaMixinMod.java @@ -0,0 +1,63 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import com.google.common.collect.ImmutableList; +import fi.dy.masa.litematica.config.Configs; +import fi.dy.masa.litematica.config.Hotkeys; +import fi.dy.masa.malilib.config.IConfigBase; +import fi.dy.masa.malilib.config.options.ConfigBoolean; +import fi.dy.masa.malilib.config.options.ConfigDouble; +import fi.dy.masa.malilib.config.options.ConfigHotkey; +import fi.dy.masa.malilib.config.options.ConfigInteger; +import fi.dy.masa.malilib.hotkeys.KeyCallbackToggleBooleanConfigWithMessage; +import fi.dy.masa.malilib.hotkeys.KeybindSettings; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.fabricmc.api.ModInitializer; + +import java.util.List; + +public class LitematicaMixinMod implements ModInitializer { + + public static Printer printer; + public static PrinterConfig printerConfig = PrinterConfig.getInstance(); + public static boolean DEBUG = false; + public static MovementHandler movementHandler = new MovementHandler(); + public static FreeLook freeLook = FreeLook.getInstance(); + // Config settings + public static final ConfigInteger PRINTING_INTERVAL = new ConfigInteger("printingInterval", 12, 1, 40, "Printing interval. Lower values mean faster printing speed.\nIf the printer creates \"ghost blocks\" or blocks are facing the wrong way, raise this value."); + public static final ConfigDouble PRINTING_RANGE = new ConfigDouble("printingRange", 5, 2.5, 5, "Printing block place range\nLower values are recommended for servers."); + // public static final ConfigBoolean PRINT_WATER = new ConfigBoolean("printWater", false, "Whether the printer should place water\n source blocks or make blocks waterlogged."); +// public static final ConfigBoolean PRINT_IN_AIR = new ConfigBoolean("printInAir", true, "Whether or not the printer should place blocks without anything to build on.\nBe aware that some anti-cheat plugins might notice this."); + public static final ConfigBoolean PRINT_MODE = new ConfigBoolean("printingMode", false, "Autobuild / print loaded selection.\nBe aware that some servers and anticheat plugins do not allow printing."); + public static final ConfigBoolean REPLACE_FLUIDS_SOURCE_BLOCKS = new ConfigBoolean("replaceFluidSourceBlocks", true, "Whether or not fluid source blocks should be replaced by the printer."); + public static final ConfigBoolean STRIP_LOGS = new ConfigBoolean("stripLogs", true, "Whether or not the printer should use normal logs if stripped\nversions are not available and then strip them with an axe."); + public static ImmutableList getConfigList() { + List list = new java.util.ArrayList<>(Configs.Generic.OPTIONS); + list.add(PRINT_MODE); + list.add(PRINTING_INTERVAL); + list.add(PRINTING_RANGE); +// list.add(PRINT_IN_AIR); + list.add(REPLACE_FLUIDS_SOURCE_BLOCKS); + list.add(STRIP_LOGS); + list.addAll(printerConfig.getOptions()); + + return ImmutableList.copyOf(list); + } + + // Hotkeys + public static final ConfigHotkey PRINT = new ConfigHotkey("print", "V", KeybindSettings.PRESS_ALLOWEXTRA_EMPTY, "Prints while pressed"); + public static final ConfigHotkey TOGGLE_PRINTING_MODE = new ConfigHotkey("togglePrintingMode", "CAPS_LOCK", KeybindSettings.PRESS_ALLOWEXTRA_EMPTY, "Allows quickly toggling on/off Printing mode"); + + public static List getHotkeyList() { + List list = new java.util.ArrayList<>(Hotkeys.HOTKEY_LIST); + list.add(PRINT); + list.add(TOGGLE_PRINTING_MODE); + + return ImmutableList.copyOf(list); + } + + @Override + public void onInitialize() { + TOGGLE_PRINTING_MODE.getKeybind().setCallback(new KeyCallbackToggleBooleanConfigWithMessage(PRINT_MODE)); + PrinterConfig.onInitialize(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java new file mode 100644 index 000000000..08860e9b8 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java @@ -0,0 +1,209 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.mixin.MixinAccessorKeyBinding; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ingame.CraftingScreen; +import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import net.minecraft.network.packet.c2s.play.PlayerInputC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import net.minecraft.util.math.Vec3d; + +public class MovementHandler { + private static final MinecraftClient mc = MinecraftClient.getInstance(); + private boolean disableNextTick = false; + + public void onGameTick() { + if (mc.player == null) return; + + // Disabled check + if (!PrinterConfig.FREE_LOOK.getBooleanValue() || !LitematicaMixinMod.PRINT_MODE.getBooleanValue()) { + if (disableNextTick) { + disableNextTick = false; + InputDirections.apply(InputDirections.getCurrentInput()); + } + return; + } + + // Disable in inventories + if (mc.currentScreen != null) { + if (PrinterConfig.MOVE_WHILE_IN_INVENTORY.getBooleanValue()) { + if (mc.currentScreen instanceof CraftingScreen || mc.currentScreen instanceof CreativeInventoryScreen) { + return; + } + } else { + return; + } + } + + // Get current inputs + InputDirections currentInputDirection = InputDirections.getCurrentInput(); + + float cameraYaw = mc.gameRenderer.getCamera().getYaw() % 360; + if (currentInputDirection != InputDirections.NONE) { + // lastPlayerInputDirection = currentInputDirection; + // Calculate resulting control direction and apply them + float inputYaw = currentInputDirection.getYaw(); + float playerYaw = (mc.player.getYaw() + 360) % 360; + + // Calculate the result yaw + float playerRelativeYaw = inputYaw + playerYaw; + float resultPlayerYaw = playerRelativeYaw - cameraYaw; + InputDirections resultDirection = InputDirections.getDirection(resultPlayerYaw); + if (resultDirection == null || resultDirection == InputDirections.NONE) { + // ChatUtil.sendClientMessage("Result direction is null"); + Printer.logger.warn("Result direction is null / None"); + return; + } + + InputDirections.apply(resultDirection); + } else /*if (overwriteKeys.getValue())*/ { +// Printer.logger.info("No input direction"); + InputDirections.apply(InputDirections.NONE); + } + } + + public static void grimRotate(ClientPlayerEntity player, float yaw, float pitch) { + Vec3d playerPos = new Vec3d(player.getX(), player.getY(), player.getZ()); + mc.getNetworkHandler().sendPacket(new PlayerInputC2SPacket(player.input.playerInput)); + mc.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.Full(playerPos.x, playerPos.y, playerPos.z, yaw, pitch, player.isOnGround(), player.horizontalCollision)); + } + + public void onDisable(ClientPlayerEntity player) { + disableNextTick = true; + } + + enum InputDirections { + FORWARD(0), + FORWARD_LEFT(45), + FORWARD_RIGHT(315), + LEFT(90), + RIGHT(270), + BACK(180), + BACK_LEFT(135), + BACK_RIGHT(225), + NONE(-1); + private final float yaw; + + InputDirections(float i) { + this.yaw = i; + } + + static InputDirections getDirection(float yaw) { + while (yaw < 0) yaw += 360; + yaw = yaw % 360; + if (yaw < 22.5) return FORWARD; + if (yaw < 67.5) return FORWARD_LEFT; + if (yaw < 112.5) return LEFT; + if (yaw < 157.5) return BACK_LEFT; + if (yaw < 202.5) return BACK; + if (yaw < 247.5) return BACK_RIGHT; + if (yaw < 292.5) return RIGHT; + if (yaw < 337.5) return FORWARD_RIGHT; + return FORWARD; + } + + /** + * Returns a unit vector in the direction of the input direction + * + * @return a unit vector in the direction of the input direction + */ + public Vec3d getVec3d() { + return new Vec3d(Math.sin(Math.toRadians(yaw)), 0, Math.cos(Math.toRadians(yaw))); + } + + public float getYaw() { + return yaw; + } + + static InputDirections getCurrentInput() { + if (isKeyPressed(mc.options.forwardKey)) { + if (isKeyPressed(mc.options.leftKey)) { + return FORWARD_LEFT; + } else if (isKeyPressed(mc.options.rightKey)) { + return FORWARD_RIGHT; + } else { + return FORWARD; + } + } else if (isKeyPressed(mc.options.backKey)) { + if (isKeyPressed(mc.options.leftKey)) { + return BACK_LEFT; + } else if (isKeyPressed(mc.options.rightKey)) { + return BACK_RIGHT; + } else { + return BACK; + } + } else if (isKeyPressed(mc.options.leftKey)) { + return LEFT; + } else if (isKeyPressed(mc.options.rightKey)) { + return RIGHT; + } else { + return NONE; + } + } + + public boolean isPressed() { + switch (this) { + case FORWARD -> isKeyPressed(mc.options.forwardKey); + case FORWARD_LEFT -> { + return isKeyPressed(mc.options.forwardKey) && isKeyPressed(mc.options.leftKey); + } + case FORWARD_RIGHT -> { + return isKeyPressed(mc.options.forwardKey) && isKeyPressed(mc.options.rightKey); + } + case LEFT -> isKeyPressed(mc.options.leftKey); + case RIGHT -> isKeyPressed(mc.options.rightKey); + case BACK -> isKeyPressed(mc.options.backKey); + case BACK_LEFT -> { + return isKeyPressed(mc.options.backKey) && isKeyPressed(mc.options.leftKey); + } + case BACK_RIGHT -> { + return isKeyPressed(mc.options.backKey) && isKeyPressed(mc.options.rightKey); + } + default -> { + return false; + } + } + return false; + } + + static void apply(InputDirections direction) { + mc.options.forwardKey.setPressed(false); + mc.options.leftKey.setPressed(false); + mc.options.rightKey.setPressed(false); + mc.options.backKey.setPressed(false); + switch (direction) { + case FORWARD -> mc.options.forwardKey.setPressed(true); + case FORWARD_LEFT -> { + mc.options.leftKey.setPressed(true); + mc.options.forwardKey.setPressed(true); + } + case FORWARD_RIGHT -> { + mc.options.rightKey.setPressed(true); + mc.options.forwardKey.setPressed(true); + } + case LEFT -> mc.options.leftKey.setPressed(true); + case RIGHT -> mc.options.rightKey.setPressed(true); + case BACK -> mc.options.backKey.setPressed(true); + case BACK_LEFT -> { + mc.options.backKey.setPressed(true); + mc.options.leftKey.setPressed(true); + } + case BACK_RIGHT -> { + mc.options.backKey.setPressed(true); + mc.options.rightKey.setPressed(true); + } + case NONE -> { + + } + } + } + } + + static boolean isKeyPressed(KeyBinding keyBinding) { + return InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow(), ((MixinAccessorKeyBinding) keyBinding).getBoundKey().getCode()); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/Printer.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/Printer.java new file mode 100644 index 000000000..993fcc8ac --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/Printer.java @@ -0,0 +1,200 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import fi.dy.masa.litematica.data.DataManager; +import fi.dy.masa.litematica.util.RayTraceUtils; +import fi.dy.masa.litematica.world.SchematicWorldHandler; +import fi.dy.masa.litematica.world.WorldSchematic; +import me.aleksilassila.litematica.printer.v1_21_4.actions.Action; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.guides.Guide; +import me.aleksilassila.litematica.printer.v1_21_4.guides.Guides; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerAbilities; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class Printer { + public static final Logger logger = LogManager.getLogger("litematica-printer"); + @NotNull + public final ClientPlayerEntity player; + MinecraftClient mc = MinecraftClient.getInstance(); + + public final ActionHandler actionHandler; + + private final Guides interactionGuides = new Guides(); + public static final InventoryManager inventoryManager = InventoryManager.getInstance(); + public static int inactivityCounter = 0; + static final LinkedList blockPosTimeout = new LinkedList<>(); + int delayCounter = 0; + @Nullable + public static Vec2f lastRotation = null; + + public Printer(@NotNull MinecraftClient client, @NotNull ClientPlayerEntity player) { + this.player = player; + + this.actionHandler = new ActionHandler(client, player); + } + + public void onMiddleClick() { + if (mc.world == null || mc.player == null) return; + BlockPos pos = RayTraceUtils.getSchematicWorldTraceIfClosest(mc.world, mc.player, 6.0); + + if (pos != null) { + WorldSchematic world = SchematicWorldHandler.getSchematicWorld(); + if (world == null) return; + inventoryManager.pickSlot(world, player, pos); + } + } + + public boolean onGameTick() { + WorldSchematic worldSchematic = SchematicWorldHandler.getSchematicWorld(); + blockPosTimeout.forEach((entry) -> entry.timer--); + blockPosTimeout.removeIf((entry) -> entry.timer <= 0); + + // If the inactivityCounter is greater than the inactive snap back value, then set the lastRotation to the current rotation + // This is used to snap back to the last rotation when the player is inactive + inactivityCounter++; + + if (worldSchematic == null) return false; + + if (PrinterConfig.TICK_DELAY.getIntegerValue() != 0 && delayCounter < PrinterConfig.TICK_DELAY.getIntegerValue()) { + delayCounter++; + return false; + } else { + delayCounter = 0; + } + + if (!LitematicaMixinMod.PRINT_MODE.getBooleanValue() && !LitematicaMixinMod.PRINT.getKeybind().isPressed()) + return false; + + PlayerAbilities abilities = player.getAbilities(); + if (!abilities.allowModifyWorld) + return false; + + if (PrinterConfig.STOP_ON_MOVEMENT.getBooleanValue() && player.getVelocity().length() > 0.1) + return false; // Stop if the player is moving + if (PrinterConfig.PRINTER_DISABLE_IN_GUIS.getBooleanValue()) { + if (mc.currentScreen != null) return false; + } + + List positions = getReachablePositions(); + + if (PrinterConfig.BLOCK_TIMEOUT.getIntegerValue() != 0) { + positions = positions.stream().filter((pos) -> blockPosTimeout.stream().noneMatch((entry) -> entry.pos.equals(pos))).toList(); // From block timeout. Don't place already placed blocks. + } + + findBlock: + for (BlockPos position : positions) { + SchematicBlockState state = new SchematicBlockState(mc.world, worldSchematic, position); + if (state.targetState.equals(state.currentState) || state.targetState.isAir()) continue; + + Guide[] guides = interactionGuides.getInteractionGuides(state); + + for (Guide guide : guides) { + if (guide.canExecute(player)) { + List actions = guide.execute(player); + actionHandler.addActions(actions.toArray(Action[]::new)); + return true; + } + if (guide.skipOtherGuides()) continue findBlock; + } + } + + return false; + } + + private List getBlocksPlayerOccupied() { + ArrayList positions = new ArrayList<>(); + BlockPos playerPos = player.getBlockPos(); + int blocksHeightOccupied = (int) Math.ceil(player.getY() + player.getHeight() - playerPos.getY()); + + positions.add(player.getBlockPos()); + positions.add(player.getBlockPos().up()); + if (blocksHeightOccupied > 2) { + positions.add(playerPos.up(2)); + } + if (Math.floor(player.getX() + player.getWidth() / 2) > playerPos.getX()) { + for (int i = 0; i < blocksHeightOccupied; i++) { + positions.add(playerPos.up(i).east()); + } + } + if ((player.getX() - player.getWidth() / 2) < playerPos.getX()) { + for (int i = 0; i < blocksHeightOccupied; i++) { + positions.add(playerPos.up(i).west()); + } + } + if (Math.floor(player.getZ() + player.getWidth() / 2) > playerPos.getZ()) { + for (int i = 0; i < blocksHeightOccupied; i++) { + positions.add(playerPos.up(i).south()); + } + } + if ((player.getZ() - player.getWidth() / 2) < playerPos.getZ()) { + for (int i = 0; i < blocksHeightOccupied; i++) { + positions.add(playerPos.up(i).north()); + } + } + return positions.stream().distinct().toList(); + } + + private List getReachablePositions() { + List playerOccupied = getBlocksPlayerOccupied(); + int maxReach = (int) Math.ceil(LitematicaMixinMod.PRINTING_RANGE.getDoubleValue()); + double maxReachSquared = MathHelper.square(LitematicaMixinMod.PRINTING_RANGE.getDoubleValue()); + + ArrayList positions = new ArrayList<>(); + + for (int y = -maxReach; y < maxReach + 1; y++) { + for (int x = -maxReach; x < maxReach + 1; x++) { + for (int z = -maxReach; z < maxReach + 1; z++) { + BlockPos blockPos = player.getBlockPos().north(x).west(z).up(y); + + if (!DataManager.getRenderLayerRange().isPositionWithinRange(blockPos)) continue; + if (this.player.getEyePos().squaredDistanceTo(Vec3d.ofCenter(blockPos)) > maxReachSquared) { + continue; + } + + positions.add(blockPos); + } + } + } + + return positions.stream() +// .filter(p -> playerOccupied.stream().noneMatch(p::equals)) + .sorted((a, b) -> { + double aDistance = new Vec3d(this.player.getX(), this.player.getY(), this.player.getZ()).squaredDistanceTo(Vec3d.ofCenter(a)); + double bDistance = new Vec3d(this.player.getX(), this.player.getY(), this.player.getZ()).squaredDistanceTo(Vec3d.ofCenter(b)); + return Double.compare(aDistance, bDistance); + }).toList(); + } + + public static void addTimeout(BlockPos pos) { + blockPosTimeout.add(new BlockTimeout(pos, PrinterConfig.BLOCK_TIMEOUT.getIntegerValue())); + } + + public void rotate(float yaw, float pitch) { + LitematicaMixinMod.freeLook.ticksSinceLastRotation = 0; + this.player.setYaw(yaw); + this.player.setPitch(pitch); + } + + public static class BlockTimeout { + int timer = 0; + BlockPos pos; + + public BlockTimeout(BlockPos pos, int timer) { + this.pos = pos; + this.timer = timer; + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicBlockState.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicBlockState.java new file mode 100644 index 000000000..886d19b73 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicBlockState.java @@ -0,0 +1,42 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import fi.dy.masa.litematica.world.WorldSchematic; +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +public class SchematicBlockState { + public final World world; + public final WorldSchematic schematic; + + public final BlockPos blockPos; + + public final BlockState targetState; + public final BlockState currentState; + + public SchematicBlockState(World world, WorldSchematic schematic, BlockPos blockPos) { + this.world = world; + this.schematic = schematic; + + this.blockPos = blockPos; + + this.targetState = schematic.getBlockState(blockPos); + this.currentState = world.getBlockState(blockPos); + } + + public SchematicBlockState offset(Direction direction) { + return new SchematicBlockState(world, schematic, blockPos.offset(direction)); + } + + @Override + public String toString() { + return "SchematicBlockState{" + + "world=" + world + + ", schematic=" + schematic + + ", blockPos=" + blockPos + + ", targetState=" + targetState + + ", currentState=" + currentState + + '}'; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java new file mode 100644 index 000000000..0f5935dfb --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java @@ -0,0 +1,24 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.litematica.util.FileType; +import me.aleksilassila.litematica.printer.v1_21_4.mixin.LitematicaSchematicAccessor; + +import java.io.File; +import java.nio.file.Path; + +/** + * @author IceTank + * @since 17.12.2024 + */ +public class SchematicConverter { + public static LitematicaSchematic convertAndReturn(File file, File out) { + LitematicaSchematic schematic = LitematicaSchematicAccessor.invokeConstructor(file.toPath(), FileType.VANILLA_STRUCTURE); + schematic.readFromFile(); + String fileName = file.getName().replace(".nbt", ""); + schematic.writeToFile(out.toPath(), fileName, true); + LitematicaSchematic newSchem = LitematicaSchematicAccessor.invokeConstructor(out.toPath().resolve(fileName + ".litematic"), FileType.LITEMATICA_SCHEMATIC); + newSchem.readFromFile(); + return newSchem; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/UpdateChecker.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/UpdateChecker.java new file mode 100644 index 000000000..28730626a --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/UpdateChecker.java @@ -0,0 +1,43 @@ +package me.aleksilassila.litematica.printer.v1_21_4; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.InputStream; +import java.net.URL; +import java.util.Scanner; + +public class UpdateChecker { + public static final String version = "v3.2.1"; + + // Try to get this to work at some point +// static { +// try (InputStream in = UpdateChecker.class.getResourceAsStream("/fabric.mod.json")) { +// String jsonString = IOUtils.toString(in, StandardCharsets.UTF_8); +// JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject(); +// System.out.println("JSON object: " + json); +// System.out.println("Raw json: " + jsonString); +// System.out.println("File: " + new File(UpdateChecker.class.getResource("/fabric.mod.json").getFile())); +// String version = json.get("version").getAsString(); +// System.out.println("Reading fabric.mod.json"); +// System.out.println("Parsed version: " + version); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + @SuppressWarnings("deprecation") + public static String getPrinterVersion() { + try (InputStream inputStream = new URL("https://api.github.com/repos/aleksilassila/litematica-printer/tags").openStream(); Scanner scanner = new Scanner(inputStream)) { + if (scanner.hasNext()) { + JsonArray tags = new JsonParser().parse(scanner.next()).getAsJsonArray(); + return ((JsonObject) tags.get(0)).get("name").getAsString(); + } + } catch (Exception exception) { + System.out.println("Cannot look for updates: " + exception.getMessage()); + } + + return ""; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/Action.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/Action.java new file mode 100644 index 000000000..72c0de908 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/Action.java @@ -0,0 +1,8 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; + +public abstract class Action { + abstract public boolean send(MinecraftClient client, ClientPlayerEntity player); +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ActionChain.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ActionChain.java new file mode 100644 index 000000000..5c5de3b28 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ActionChain.java @@ -0,0 +1,35 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; + +import java.util.ArrayList; +import java.util.List; + +public class ActionChain extends Action { + List actionsCurrentTick = new ArrayList<>(); + List actionsNextTick = new ArrayList<>(); + + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + throw new RuntimeException("ActionChain should not be sent. Manually send each preTick and postTick action."); + } + + public void addImmediateAction(Action action) { + actionsCurrentTick.add(action); + } + public void addNextTickAction(Action action) { + actionsNextTick.add(action); + } + + public void clear() { + actionsCurrentTick.clear(); + } + + public List getActionsCurrentTick() { + return actionsCurrentTick; + } + public List getActionsNextTick() { + return actionsNextTick; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/InteractAction.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/InteractAction.java new file mode 100644 index 000000000..973574649 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/InteractAction.java @@ -0,0 +1,39 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; + +abstract public class InteractAction extends Action { + public final PrinterPlacementContext context; + + public InteractAction(PrinterPlacementContext context) { + this.context = context; + } + + protected abstract ActionResult interact(MinecraftClient client, ClientPlayerEntity player, Hand hand, BlockHitResult hitResult); + + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + interact(client, player, Hand.MAIN_HAND, context.hitResult); + + if (LitematicaMixinMod.DEBUG) + System.out.println("InteractAction.send: Blockpos: " + context.getBlockPos() + " Side: " + context.getSide() + " HitPos: " + context.getHitPos()); + BlockPos pos = context.hitResult.isInsideBlock() ? context.hitResult.getBlockPos() : context.hitResult.getBlockPos().offset(context.hitResult.getSide()); + Printer.addTimeout(pos); + return true; + } + + @Override + public String toString() { + return "InteractAction{" + + "context=" + context + + '}'; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PostAction.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PostAction.java new file mode 100644 index 000000000..62e4e7d94 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PostAction.java @@ -0,0 +1,30 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; + +public class PostAction extends PrepareAction { + public PostAction(PrinterPlacementContext context, ClientPlayerEntity player) { + super(context); + this.pitch = player.getPitch(); + this.yaw = player.getYaw(); + } + + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + if (context.canStealth) { + PlayerMoveC2SPacket.LookAndOnGround packet = new PlayerMoveC2SPacket.LookAndOnGround(this.yaw, this.pitch, player.isOnGround(), player.horizontalCollision); + + if (PrinterConfig.ROTATE_PLAYER.getBooleanValue()) { + player.setYaw(this.yaw); + player.setPitch(this.pitch); + } + + player.networkHandler.sendPacket(packet); + } + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PrepareAction.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PrepareAction.java new file mode 100644 index 000000000..ae8c4bd86 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PrepareAction.java @@ -0,0 +1,120 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +public class PrepareAction extends Action { +// public final Direction lookDirection; +// public final boolean requireSneaking; +// public final Item item; + +// public PrepareAction(Direction lookDirection, boolean requireSneaking, Item item) { +// this.lookDirection = lookDirection; +// this.requireSneaking = requireSneaking; +// this.item = item; +// } +// +// public PrepareAction(Direction lookDirection, boolean requireSneaking, BlockState requiredState) { +// this(lookDirection, requireSneaking, requiredState.getBlock().asItem()); +// } + + public final PrinterPlacementContext context; + + public boolean modifyYaw = true; + public boolean modifyPitch = true; + public float yaw = 0; + public float pitch = 0; + + public PrepareAction(PrinterPlacementContext context) { + this.context = context; + + @Nullable + Direction lookDirection = context.lookDirection; + + if (lookDirection != null && lookDirection.getAxis().isHorizontal()) { + this.yaw = lookDirection.getUnitVector().y == 0 ? 0 : 90 * lookDirection.getUnitVector().y; + } else { + this.modifyYaw = false; + } + + if (lookDirection == Direction.UP) { + this.pitch = -90; + } else if (lookDirection == Direction.DOWN) { + this.pitch = 90; + } else if (lookDirection != null) { + this.pitch = 0; + } else { + this.modifyPitch = false; + } + } + + public PrepareAction(PrinterPlacementContext context, float yaw, float pitch) { + this.context = context; + + this.yaw = yaw; + this.pitch = pitch; + } + + static float[] getNeededRotations(ClientPlayerEntity player, Vec3d vec) { + Vec3d eyesPos = player.getEyePos(); + + double diffX = vec.x - eyesPos.x; + double diffY = vec.y - eyesPos.y; + double diffZ = vec.z - eyesPos.z; + + double r = Math.sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ); + double yaw = -Math.atan2(diffX, diffZ) / Math.PI * 180; + + double pitch = -Math.asin(diffY / r) / Math.PI * 180; + +// double diffXZ = Math.sqrt(diffX * diffX + diffZ * diffZ); +// +// float yaw = (float) Math.toDegrees(Math.atan2(diffZ, diffX)) - 90F; +// float pitch = (float) -Math.toDegrees(Math.atan2(diffY, diffXZ)); +// return new float[]{player.getYaw() + MathHelper.wrapDegrees(yaw - player.getYaw()), player.getPitch() + MathHelper.wrapDegrees(pitch - player.getPitch()) +// }; + return new float[]{(float) yaw, (float) pitch}; + } + + public static float clamp(float val, float min, float max) { + return Math.max(min, Math.min(max, val)); + } + + /** + * Returns the yaw difference between two yaw values. + * @param yaw1 The first yaw value. + * @param yaw2 The second yaw value. + * @return The yaw difference. + */ + public static float deltaYaw (float yaw1, float yaw2) { + final float PI_2 = (float) (Math.PI * 2); + float dYaw = (yaw1 - yaw2) % PI_2; + if (dYaw < -Math.PI) dYaw += PI_2; + else if (dYaw > Math.PI) dYaw -= PI_2; + + return dYaw; + } + + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + ItemStack itemStack = context.getStack(); + + if (!Printer.inventoryManager.select(itemStack)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "PrepareAction{" + + "context=" + context + + '}'; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PrepareLook.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PrepareLook.java new file mode 100644 index 000000000..ca9501248 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PrepareLook.java @@ -0,0 +1,116 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.MovementHandler; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; + +import java.util.Optional; + +public class PrepareLook extends Action { + private static final MinecraftClient mc = MinecraftClient.getInstance(); + public final PrinterPlacementContext context; + public Optional yaw = Optional.empty(); + public Optional pitch = Optional.empty(); + + public PrepareLook(PrinterPlacementContext context) { + this.context = context; + } + + static float[] getNeededRotations(ClientPlayerEntity player, Vec3d vec) { + Vec3d eyesPos = player.getEyePos(); + + double diffX = vec.x - eyesPos.x; + double diffY = vec.y - eyesPos.y; + double diffZ = vec.z - eyesPos.z; + + double r = Math.sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ); + double yaw = -Math.atan2(diffX, diffZ) / Math.PI * 180; + + double pitch = -Math.asin(diffY / r) / Math.PI * 180; + + return new float[]{(float) yaw, (float) pitch}; + } + + static float[] getRotation(Direction direction) { + switch (direction) { + case NORTH: + return new float[]{180.0f, 0.0f}; + case SOUTH: + return new float[]{0.0f, 0.0f}; + case WEST: + return new float[]{90.0f, 0.0f}; + case EAST: + return new float[]{-90.0f, 0.0f}; + case UP: + return new float[]{0.0f, -90.0f}; + case DOWN: + return new float[]{0.0f, 90.0f}; + default: + return new float[]{0.0f, 0.0f}; + } + } + + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + if (context.isAirPlace) { + if (context.lookDirection != null) { + float[] targetRot = getRotation(context.lookDirection); + this.yaw = Optional.of(targetRot[0]); + this.pitch = Optional.of(targetRot[1]); + + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) + Printer.logger.info("Sending yaw for modified airplace yaw: " + yaw + ", pitch: " + pitch); + + if (PrinterConfig.PRINTER_GRIM_ROTATION.getBooleanValue()) { + MovementHandler.grimRotate(player, yaw.get(), pitch.get()); + } else if (PrinterConfig.ROTATE_PLAYER.getBooleanValue()) { + LitematicaMixinMod.printer.rotate(yaw.get(), pitch.get()); + } else { + mc.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround( + yaw.get(), pitch.get(), player.isOnGround(), player.horizontalCollision)); + } + return true; + } + } + if (context.canStealth) { + float[] targetRot = getNeededRotations(player, context.getHitPos()); + + if (PrinterConfig.ROTATE_PLAYER.getBooleanValue()) { + LitematicaMixinMod.printer.rotate(targetRot[0], targetRot[1]); + } + + this.yaw = Optional.of(targetRot[0]); + this.pitch = Optional.of(targetRot[1]); + if (PrinterConfig.PRINTER_GRIM_ROTATION.getBooleanValue()) { + MovementHandler.grimRotate(player, targetRot[0], targetRot[1]); + } + } else { + float yaw = player.getYaw(); + float pitch = player.getPitch(); + + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) + System.out.println("Sending yaw for modified yaw: " + yaw + ", pitch: " + pitch); + + this.yaw = Optional.of(yaw); + this.pitch = Optional.of(pitch); + if (PrinterConfig.PRINTER_GRIM_ROTATION.getBooleanValue()) { + MovementHandler.grimRotate(player, yaw, pitch); + } + } + return true; + } + + @Override + public String toString() { + return "PrepareAction{" + + "context=" + context + + '}'; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java new file mode 100644 index 000000000..604cdbb53 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java @@ -0,0 +1,17 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.network.packet.c2s.play.PlayerInputC2SPacket; +import net.minecraft.util.PlayerInput; + +public class PresShift extends Action { + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + player.input.playerInput = new PlayerInput(player.input.playerInput.forward(), + player.input.playerInput.backward(), player.input.playerInput.left(), player.input.playerInput.right(), + player.input.playerInput.jump(), true, player.input.playerInput.sprint()); + player.networkHandler.sendPacket(new PlayerInputC2SPacket(player.input.playerInput)); + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java new file mode 100644 index 000000000..adad0c416 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java @@ -0,0 +1,18 @@ +package me.aleksilassila.litematica.printer.v1_21_4.actions; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.network.packet.c2s.play.PlayerInputC2SPacket; +import net.minecraft.util.PlayerInput; + +public class ReleaseShiftAction extends Action { + private static final MinecraftClient mc = MinecraftClient.getInstance(); + @Override + public boolean send(MinecraftClient client, ClientPlayerEntity player) { + player.input.playerInput = new PlayerInput(player.input.playerInput.forward(), + player.input.playerInput.backward(), player.input.playerInput.left(), player.input.playerInput.right(), + player.input.playerInput.jump(), mc.options.sneakKey.isPressed(), player.input.playerInput.sprint()); + player.networkHandler.sendPacket(new PlayerInputC2SPacket(player.input.playerInput)); + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/FreeLookKeyCallbackToggle.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/FreeLookKeyCallbackToggle.java new file mode 100644 index 000000000..04e530b62 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/FreeLookKeyCallbackToggle.java @@ -0,0 +1,26 @@ +package me.aleksilassila.litematica.printer.v1_21_4.config; + +import fi.dy.masa.malilib.config.options.ConfigBoolean; +import fi.dy.masa.malilib.hotkeys.IKeybind; +import fi.dy.masa.malilib.hotkeys.KeyAction; +import fi.dy.masa.malilib.hotkeys.KeyCallbackToggleBooleanConfigWithMessage; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import net.minecraft.client.MinecraftClient; + +public class FreeLookKeyCallbackToggle extends KeyCallbackToggleBooleanConfigWithMessage { + MinecraftClient mc = MinecraftClient.getInstance(); + public FreeLookKeyCallbackToggle(ConfigBoolean config) { + super(config); + } + + @Override + public boolean onKeyAction(KeyAction action, IKeybind key) { + super.onKeyAction(action, key); + + if (!config.getBooleanValue() && mc.player != null) { + LitematicaMixinMod.movementHandler.onDisable(mc.player); + } + + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterConfig.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterConfig.java new file mode 100644 index 000000000..bac08677a --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterConfig.java @@ -0,0 +1,185 @@ +package me.aleksilassila.litematica.printer.v1_21_4.config; + +import com.google.common.collect.ImmutableList; +import fi.dy.masa.litematica.config.Configs; +import fi.dy.masa.malilib.MaLiLib; +import fi.dy.masa.malilib.config.IConfigBase; +import fi.dy.masa.malilib.config.IConfigOptionListEntry; +import fi.dy.masa.malilib.config.options.*; +import fi.dy.masa.malilib.event.InputEventHandler; +import fi.dy.masa.malilib.hotkeys.KeybindSettings; +import me.aleksilassila.litematica.printer.v1_21_4.InventoryManager; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.config.Configurator; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class PrinterConfig { + private PrinterConfig() { + } + + @Nullable + public static PrinterConfig INSTANCE; + + public static PrinterConfig getInstance() { + if (INSTANCE == null) { + INSTANCE = new PrinterConfig(); + } + return INSTANCE; + } + + public static final ConfigInteger TICK_DELAY = new ConfigInteger("printerTickDelay", 0, 0, 100, "Tick delay between actions. 0 = no delay. Printer is limited to one action per tick."); + public static final ConfigInteger BLOCK_TIMEOUT = new ConfigInteger("printerBlockTimeout", 10, 0, 100, "How many ticks to wait before trying to place the same block again."); + public static final ConfigBoolean ROTATE_PLAYER = new ConfigBoolean("printerRotatePlayer", false, "Rotate the player to face the block to place."); + public static final ConfigBoolean PRINTER_GRIM_ROTATION = new ConfigBoolean("printerGrimRotate", true, "Allows you to place blocks anywhere while walking or using baritone."); + public static final ConfigBoolean STOP_ON_MOVEMENT = new ConfigBoolean("printerStopOnMovement", false, "Stop the printer if the player velocity is to high."); + public static final ConfigInteger INVENTORY_DELAY = new ConfigInteger("printerInventoryDelay", 10, 0, 100, "The delay between each inventory action. 0 = no delay."); + public static final ConfigOptionList PRINTER_INVENTORY_MANAGEMENT_MODE = new ConfigOptionList("printerInventoryManagementMode", InventoryManagementModeEnum.LEAST_USED, "Inventory management mode. Rolling = cycle through the hotbar, Least Used = use the least used slot in the hotbar."); + public static final ConfigBoolean RAYCAST = new ConfigBoolean("printerRaycast", false, "Raycast the block to place to check if it is obstructed by another block. Most anti cheat don't check for this."); + public static final ConfigBoolean NO_PLACEMENT_CACHE = new ConfigBoolean("printerNoPlacementCache", false, "Disable the placement cache. This may cause more lag."); + public static final ConfigBoolean RAYCAST_STRICT_BLOCK_HIT = new ConfigBoolean("printerRaycastStrictBlockHit", false, "Check if the right side of the block is hit when raycasting."); + public static final ConfigBoolean PREVENT_DOUBLE_TAP_SPRINTING = new ConfigBoolean("printerPreventDoubleTapSprinting", false, "Prevent double tap sprinting when the printer is active."); + public static final ConfigBoolean MOVE_WHILE_IN_INVENTORY = new ConfigBoolean("printerMoveWhileInInventory", false, "Allows the player to move while the player inventory is open."); + public static final ConfigBoolean FREE_LOOK = new ConfigBoolean("printerFreeLook", false, "Free look mode. Allows you to look around while the printer is active."); + public static final ConfigHotkey FREE_LOOK_TOGGLE = new ConfigHotkey("printerFreeLookToggle", "", KeybindSettings.MODIFIER_INGAME, "Free look mode. Allows you to look around while the printer is active."); + public static final ConfigBoolean FREE_LOOK_THIRD_PERSON = new ConfigBoolean("printerFreeLookThirdPerson", true, "Auto switches the camera to third person when free look is enabled."); + public static final ConfigInteger FREE_LOOK_LOOK_BACK = new ConfigInteger("printerFreeLookLookBackDelay", 1, 0, 100, "Time in ticks until the player character is rotated back to the camera view. 0 to disable."); + public static final ConfigBoolean FREE_LOOK_LOOK_BACK_ALWAYS_ROTATE_PLAYER = new ConfigBoolean("printerFreeLookLookBackAlwaysRotatePlayer", false, "Always rotate the player back to the camera view.\nMakes it more compatible with Baritone edge cases but is more intrusive and might cause other issues."); + public static final ConfigBoolean STRICT_BLOCK_FACE_CHECK = new ConfigBoolean("printerStrictBlockFaceCheck", true, "Places only against block faces that are facing the player."); + public static final ConfigHotkey PRINTER_PICK_BLOCK = new ConfigHotkey("printerPickBlock", "MIDDLE_MOUSE", KeybindSettings.PRESS_ALLOWEXTRA_EMPTY, "Pick a block from the schematic preview to the hotbar.\nAlso works for materials in shulkers when PICK_BLOCK_SHULKERS is enabled."); + public static final ConfigBoolean PRINTER_DEBUG_LOG = new ConfigBoolean("printerDebugLog", false, "Print debug messages to the console."); + public static final ConfigBoolean PRINTER_IGNORE_ROTATION = new ConfigBoolean("printerIgnoreRotation", false, "Ignore the block rotation when placing."); + public static final ConfigBoolean PRINTER_ALLOW_NONE_EXACT_STATES = new ConfigBoolean("printerAllowNoneExactStates", false, "Allow none exact block states to be placed.\nThis includes things like lichen, muchroom stems, etc."); + public static final ConfigBoolean PRINTER_DISABLE_IN_GUIS = new ConfigBoolean("printerDisableInGuis", true, "Disable the printer in GUIs."); + public static final ConfigBoolean PRINTER_AIRPLACE = new ConfigBoolean("printerAirPlace", false, "Place blocks in the air."); + public static final ConfigBoolean PRINTER_AIRPLACE_ONLY = new ConfigBoolean("printerAirPlaceOnly", false, "Attempt to air place only when air place is enabled."); + public static final ConfigBoolean PRINTER_AIRPLACE_OFFHAND_SLOT_SUPPRESS = new ConfigBoolean("printerAirPlaceOffhandSlotSuppress", true, "Suppress off-hand slot updates when air placing. Turn off when there are de-sync issues in the off-hand slot."); + public static final ConfigBoolean PRINTER_SUPER_CHINESE_GHOST_ITEM_FIX = new ConfigBoolean("printerSuperChineseGhostItemFix", true, "Fixes ghost items in the inventory when placing a lot of blocks really fast."); + public static final ConfigDouble PRINTER_AIRPLACE_RANGE = new ConfigDouble("printerAirPlaceRange", 5, 0, 10, "Range at which the printer can air place at"); + public static final ConfigBoolean PRINTER_AIRPLACE_FLOATING_ONLY = new ConfigBoolean("printerAirPlaceFloatingOnly", false, "Only attempt to air place if the block position is surrounded by air."); + public static final ConfigInteger PRINTER_MIN_INACTIVE_TIME_AIR_PLACE = new ConfigInteger("printerMinInactiveTimeAirPlace", 0, "Minimum time in ticks to wait before placing a block in the air."); + public static final ConfigString PRINTER_HOTBAR_SLOTS = new ConfigString("printerHotbarSlots", "3,4,5,6,7,8,9", "Hotbar slots to use for the printer. Numbers from 1-9 separated by commas."); + public static final ConfigBoolean AUTO_CONVERT_SCHEMATIC_TO_LITEMATIC_ON_LOAD = new ConfigBoolean("autoConvertSchematicToLitematicOnLoad", false, "Automatically convert schematic files to litematic files when loading them."); + public static final ConfigBoolean PRINTER_PLACE_OBSERVERS_LAST = new ConfigBoolean("printerPlaceObserversLast", false, "Only place observers when the block infront of them in the schematic is either air or already placed in the world."); + public static final ConfigBoolean WATERLOGGING = new ConfigBoolean("printerWaterlogging", false, "Automatically waterlog blocks with water buckets that should be waterlogged in the schematic."); + + public ImmutableList getOptions() { + List list = new java.util.ArrayList<>(Configs.Generic.OPTIONS); + list.add(TICK_DELAY); + list.add(BLOCK_TIMEOUT); + list.add(ROTATE_PLAYER); + list.add(PRINTER_GRIM_ROTATION); + list.add(STOP_ON_MOVEMENT); + list.add(INVENTORY_DELAY); + list.add(PRINTER_INVENTORY_MANAGEMENT_MODE); + list.add(RAYCAST); + list.add(NO_PLACEMENT_CACHE); + list.add(RAYCAST_STRICT_BLOCK_HIT); + list.add(PREVENT_DOUBLE_TAP_SPRINTING); + list.add(MOVE_WHILE_IN_INVENTORY); + list.add(FREE_LOOK); + list.add(FREE_LOOK_TOGGLE); + list.add(STRICT_BLOCK_FACE_CHECK); + list.add(FREE_LOOK_THIRD_PERSON); + list.add(FREE_LOOK_LOOK_BACK); + list.add(FREE_LOOK_LOOK_BACK_ALWAYS_ROTATE_PLAYER); + list.add(PRINTER_PICK_BLOCK); + list.add(PRINTER_DEBUG_LOG); + list.add(PRINTER_IGNORE_ROTATION); + list.add(PRINTER_ALLOW_NONE_EXACT_STATES); + list.add(PRINTER_DISABLE_IN_GUIS); + list.add(PRINTER_AIRPLACE); + list.add(PRINTER_AIRPLACE_ONLY); + list.add(PRINTER_AIRPLACE_OFFHAND_SLOT_SUPPRESS); + list.add(PRINTER_SUPER_CHINESE_GHOST_ITEM_FIX); + list.add(PRINTER_AIRPLACE_RANGE); + list.add(PRINTER_AIRPLACE_FLOATING_ONLY); + list.add(PRINTER_MIN_INACTIVE_TIME_AIR_PLACE); + list.add(PRINTER_HOTBAR_SLOTS); + list.add(AUTO_CONVERT_SCHEMATIC_TO_LITEMATIC_ON_LOAD); + list.add(PRINTER_PLACE_OBSERVERS_LAST); + list.add(WATERLOGGING); + + PRINTER_DEBUG_LOG.setValueChangeCallback(config -> { + if (config.getBooleanValue()) { + MaLiLib.LOGGER.info("Printer debug logging enabled"); + Configurator.setLevel(LogManager.getLogger(Printer.logger.getName()), Level.DEBUG); + } else { + MaLiLib.LOGGER.info("Printer debug logging disabled"); + Configurator.setLevel(LogManager.getLogger(Printer.logger.getName()), Level.INFO); + } + }); + + PRINTER_HOTBAR_SLOTS.setValueChangeCallback(InventoryManager::setHotbarSlots); + + return ImmutableList.copyOf(list); + } + + public static boolean isDebug() { + return PRINTER_DEBUG_LOG.getBooleanValue(); + } + + public static void onInitialize() { + MaLiLib.LOGGER.info("PrinterConfig.onInitialize"); + FREE_LOOK_TOGGLE.getKeybind().setCallback(new FreeLookKeyCallbackToggle(FREE_LOOK)); + PRINTER_PICK_BLOCK.getKeybind().setCallback(new PrinterPickBlockKeyCallback()); + InputEventHandler.getKeybindManager().registerKeybindProvider(PrinterInputHandler.getInstance()); + } + + public static void onConfigFileLoad() { + InventoryManager.setHotbarSlots(PRINTER_HOTBAR_SLOTS); + } + + public enum InventoryManagementModeEnum implements IConfigOptionListEntry { + ROLLING("rolling", "Rolling"), + LEAST_USED("leastUsed", "Least Used"); + + final String name; + final String displayName; + + InventoryManagementModeEnum(String name, String displayName) { + this.name = name; + this.displayName = displayName; + } + + public boolean is(InventoryManagementModeEnum mode) { + return this.ordinal() == mode.ordinal(); + } + + public boolean is(String string) { + return this.name.equalsIgnoreCase(string); + } + + @Override + public String getStringValue() { + return name; + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public IConfigOptionListEntry cycle(boolean forward) { + int current = this.ordinal(); + int next = forward ? current + 1 : current - 1; + if (next < 0) next = InventoryManagementModeEnum.values().length - 1; + if (next >= InventoryManagementModeEnum.values().length) next = 0; + return InventoryManagementModeEnum.values()[next]; + } + + @Override + public IConfigOptionListEntry fromString(String value) { + for (InventoryManagementModeEnum mode : InventoryManagementModeEnum.values()) { + if (mode.name.equalsIgnoreCase(value)) { + return mode; + } + } + return this; + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterInputHandler.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterInputHandler.java new file mode 100644 index 000000000..55c80f315 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterInputHandler.java @@ -0,0 +1,25 @@ +package me.aleksilassila.litematica.printer.v1_21_4.config; + +import fi.dy.masa.malilib.hotkeys.IKeybindManager; +import fi.dy.masa.malilib.hotkeys.IKeybindProvider; + +public class PrinterInputHandler implements IKeybindProvider { + static PrinterInputHandler INSTANCE; + + public static PrinterInputHandler getInstance() { + if (INSTANCE == null) { + INSTANCE = new PrinterInputHandler(); + } + return INSTANCE; + } + @Override + public void addKeysToMap(IKeybindManager manager) { + manager.addKeybindToMap(PrinterConfig.FREE_LOOK_TOGGLE.getKeybind()); + manager.addKeybindToMap(PrinterConfig.PRINTER_PICK_BLOCK.getKeybind()); + } + + @Override + public void addHotkeys(IKeybindManager manager) { + + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterPickBlockKeyCallback.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterPickBlockKeyCallback.java new file mode 100644 index 000000000..74af8eb52 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/config/PrinterPickBlockKeyCallback.java @@ -0,0 +1,14 @@ +package me.aleksilassila.litematica.printer.v1_21_4.config; + +import fi.dy.masa.malilib.hotkeys.IHotkeyCallback; +import fi.dy.masa.malilib.hotkeys.IKeybind; +import fi.dy.masa.malilib.hotkeys.KeyAction; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; + +public class PrinterPickBlockKeyCallback implements IHotkeyCallback { + @Override + public boolean onKeyAction(KeyAction action, IKeybind key) { + LitematicaMixinMod.printer.onMiddleClick(); + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java new file mode 100644 index 000000000..3b4441c84 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java @@ -0,0 +1,147 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.actions.Action; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.guides.placement.PropertySpecificGuesserGuide; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.BlockHelperImpl; +import net.minecraft.block.BlockState; +import net.minecraft.block.CoralBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.state.property.Properties; +import net.minecraft.state.property.Property; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +abstract public class Guide extends BlockHelperImpl { + protected final SchematicBlockState state; + protected final BlockState currentState; + protected final BlockState targetState; + + public Guide(SchematicBlockState state) { + this.state = state; + + this.currentState = state.currentState; + this.targetState = state.targetState; + } + + protected boolean playerHasRightItem(ClientPlayerEntity player) { + return getRequiredItemStackSlot(player) != -1; + } + + public int getSlotWithItem(ClientPlayerEntity player, ItemStack itemStack) { + PlayerInventory inventory = player.getInventory(); + + for (int i = 0; i < 36; ++i) { + if (itemStack.isEmpty() && inventory.getStack(i).isOf(itemStack.getItem())) return i; + if (!inventory.getStack(i).isEmpty() && ItemStack.areItemsEqual(inventory.getStack(i), itemStack)) { + return i; + } + } + + return -1; + } + + protected int getRequiredItemStackSlot(ClientPlayerEntity player) { + if (player.getAbilities().creativeMode) { + return player.getInventory().getSelectedSlot(); + } + + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + if (requiredItem.isEmpty()) return -1; + + return getSlotWithItem(player, requiredItem); + } + + public boolean canExecute(ClientPlayerEntity player) { + if (!playerHasRightItem(player)) return false; + + BlockState targetState = state.targetState; + BlockState currentState = state.currentState; + + return !statesEqual(targetState, currentState); + } + + abstract public @NotNull List execute(ClientPlayerEntity player); + + abstract protected @NotNull List getRequiredItems(); + + /** + * Returns the first required item that player has access to, + * or empty if the items are inaccessible. + */ + protected List getRequiredItem(ClientPlayerEntity player) { + List requiredItems = getRequiredItems(); + + for (ItemStack requiredItem : requiredItems) { + if (player.getAbilities().creativeMode) return List.of(requiredItem); + + int slot = getSlotWithItem(player, requiredItem); + if (slot > -1) + return List.of(requiredItem); + } + + return List.of(); + } + + protected boolean statesEqualIgnoreProperties(BlockState state1, BlockState state2, Property... propertiesToIgnore) { + if (state1.getBlock() != state2.getBlock()) return false; + + loop: + for (Property property : state1.getProperties()) { + if (!PrinterConfig.WATERLOGGING.getBooleanValue() && property == Properties.WATERLOGGED && !(state1.getBlock() instanceof CoralBlock)) continue; + + for (Property ignoredProperty : propertiesToIgnore) { + if (property == ignoredProperty) continue loop; + } + + try { + if (state1.get(property) != state2.get(property)) { + return false; + } + } catch (Exception e) { + return false; + } + } + + return true; + } + + protected static > Optional getProperty(BlockState blockState, Property property) { + if (blockState.contains(property)) { + return Optional.of(blockState.get(property)); + } + return Optional.empty(); + } + + /** + * Returns true if the two states are equal, ignoring properties that are not relevant + */ + protected boolean statesEqual(BlockState state1, BlockState state2) { + // Always ignore redstone-volatile properties that can differ transiently in-world + // to avoid blocking placement under redstone power. + Property[] redstoneVolatile = new Property[] { + Properties.POWERED, // many blocks (buttons, rails, etc.) + Properties.TRIGGERED, // dispensers/dropper + Properties.ENABLED // hoppers + }; + + if (PrinterConfig.PRINTER_IGNORE_ROTATION.getBooleanValue()) { + // Merge rotation ignore with redstone-volatile ignores + Property[] merged = new Property[PropertySpecificGuesserGuide.rotationProperties.length + redstoneVolatile.length]; + System.arraycopy(PropertySpecificGuesserGuide.rotationProperties, 0, merged, 0, PropertySpecificGuesserGuide.rotationProperties.length); + System.arraycopy(redstoneVolatile, 0, merged, PropertySpecificGuesserGuide.rotationProperties.length, redstoneVolatile.length); + return statesEqualIgnoreProperties(state1, state2, merged); + } else { + return statesEqualIgnoreProperties(state1, state2, redstoneVolatile); + } + } + + public boolean skipOtherGuides() { + return false; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guides.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guides.java new file mode 100644 index 000000000..080b1d544 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guides.java @@ -0,0 +1,81 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.guides.interaction.*; +import me.aleksilassila.litematica.printer.v1_21_4.guides.placement.*; +import net.minecraft.block.*; +import net.minecraft.util.Pair; + +import java.util.ArrayList; + +public class Guides { + protected final static ArrayList, Class[]>> guides = new ArrayList<>(); + + @SafeVarargs + protected static void registerGuide(Class guideClass, Class... blocks) { + guides.add(new Pair<>(guideClass, blocks)); + } + + static { +// registerGuide(SkipGuide.class, AbstractSignBlock.class, SkullBlock.class, BannerBlock.class); + + registerGuide(RotatingBlockGuide.class, AbstractSkullBlock.class, AbstractSignBlock.class, AbstractBannerBlock.class); + registerGuide(FacingBlockGuide.class, StairsBlock.class, GlazedTerracottaBlock.class); + registerGuide(SlabGuide.class, SlabBlock.class); + registerGuide(TorchGuide.class, TorchBlock.class, WallRedstoneTorchBlock.class); + registerGuide(FarmlandGuide.class, FarmlandBlock.class); + registerGuide(TillingGuide.class, FarmlandBlock.class); + registerGuide(RailGuesserGuide.class, AbstractRailBlock.class); + registerGuide(ChestGuide.class, ChestBlock.class); + registerGuide(FlowerPotGuide.class, FlowerPotBlock.class); + registerGuide(FlowerPotFillGuide.class, FlowerPotBlock.class); + + registerGuide(FallingBlockGuide.class, FallingBlock.class); + registerGuide(BlockIndifferentGuesserGuide.class, BambooBlock.class, BigDripleafStemBlock.class, BigDripleafBlock.class, + TwistingVinesPlantBlock.class, TripwireBlock.class, MushroomBlock.class, MultifaceGrowthBlock.class); + + registerGuide(CampfireExtinguishGuide.class, CampfireBlock.class); + registerGuide(LightCandleGuide.class, AbstractCandleBlock.class); + registerGuide(EnderEyeGuide.class, EndPortalFrameBlock.class); + + // Catch most blocks + registerGuide(PropertySpecificGuesserGuide.class); +// registerGuide(LogGuide.class); + + // Anything that replaces or changes stuff after placement + registerGuide(CycleStateGuide.class, + DoorBlock.class, FenceGateBlock.class, TrapdoorBlock.class, + LeverBlock.class, + RepeaterBlock.class, ComparatorBlock.class, NoteBlock.class); + registerGuide(BlockReplacementGuide.class, SnowBlock.class, SeaPickleBlock.class, CandleBlock.class, SlabBlock.class); + registerGuide(WaterLogGuide.class); + registerGuide(LogStrippingGuide.class); + } + + public ArrayList, Class[]>> getGuides() { + return guides; + } + + public Guide[] getInteractionGuides(SchematicBlockState state) { + ArrayList, Class[]>> guides = getGuides(); + + ArrayList applicableGuides = new ArrayList<>(); + for (Pair, Class[]> guidePair : guides) { + try { + if (guidePair.getRight().length == 0) { + applicableGuides.add(guidePair.getLeft().getConstructor(SchematicBlockState.class).newInstance(state)); + continue; + } + + for (Class clazz : guidePair.getRight()) { + if (clazz.isInstance(state.targetState.getBlock())) { + applicableGuides.add(guidePair.getLeft().getConstructor(SchematicBlockState.class).newInstance(state)); + } + } + } catch (Exception ignored) { + } + } + + return applicableGuides.toArray(Guide[]::new); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/SkipGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/SkipGuide.java new file mode 100644 index 000000000..ffd21a719 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/SkipGuide.java @@ -0,0 +1,37 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.actions.Action; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SkipGuide extends Guide { + public SkipGuide(SchematicBlockState state) { + super(state); + } + + @Override + public boolean skipOtherGuides() { + return true; + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + return false; + } + + @Override + public @NotNull List execute(ClientPlayerEntity player) { + return new ArrayList<>(); + } + + @Override + protected @NotNull List getRequiredItems() { + return Collections.singletonList(ItemStack.EMPTY); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/CampfireExtinguishGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/CampfireExtinguishGuide.java new file mode 100644 index 000000000..deea1056f --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/CampfireExtinguishGuide.java @@ -0,0 +1,34 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.CampfireBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +public class CampfireExtinguishGuide extends InteractionGuide { + boolean shouldBeLit; + boolean isLit; + + public CampfireExtinguishGuide(SchematicBlockState state) { + super(state); + + shouldBeLit = getProperty(targetState, CampfireBlock.LIT).orElse(false); + isLit = getProperty(currentState, CampfireBlock.LIT).orElse(false); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + + return (currentState.getBlock() instanceof CampfireBlock) && !shouldBeLit && isLit; + } + + @Override + protected @NotNull List getRequiredItems() { + return Arrays.stream(SHOVEL_ITEMS).map(ItemStack::new).toList(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/CycleStateGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/CycleStateGuide.java new file mode 100644 index 000000000..cce0e8b60 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/CycleStateGuide.java @@ -0,0 +1,59 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.*; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.state.property.Properties; +import net.minecraft.state.property.Property; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class CycleStateGuide extends InteractionGuide { + private static final Property[] propertiesToIgnore = new Property[]{ + Properties.POWERED, + Properties.LIT, + Properties.BLOCK_FACE, + Properties.FACING, + Properties.LOCKED, + Properties.BLOCK_HALF, + Properties.DOOR_HINGE, + Properties.IN_WALL, + RepeaterBlock.FACING + }; + + public CycleStateGuide(SchematicBlockState state) { + super(state); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!playerHasRightItem(player)) return false; + + if (currentState.getBlock() == Blocks.IRON_TRAPDOOR) { + return false; // Iron trapdoors cannot be toggled by interaction + } + + if (currentState.getBlock() != targetState.getBlock()) { + return false; // Different blocks cannot be toggled + } + + BlockState targetState = state.targetState; + BlockState currentState = state.currentState; + + if (currentState.getBlock() == Blocks.LEVER) { + if (currentState.get(LeverBlock.POWERED) == targetState.get(LeverBlock.POWERED)) { + return false; // Lever blocks must be toggled if POWERED property is incorrect, regardless of other properties + } + return true; + } + return !statesEqualIgnoreProperties(targetState, currentState, propertiesToIgnore); + } + + @Override + protected @NotNull List getRequiredItems() { + return Collections.singletonList(ItemStack.EMPTY); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/EnderEyeGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/EnderEyeGuide.java new file mode 100644 index 000000000..caffef5d1 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/EnderEyeGuide.java @@ -0,0 +1,33 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.state.property.Properties; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class EnderEyeGuide extends InteractionGuide { + public EnderEyeGuide(SchematicBlockState state) { + super(state); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + + if (currentState.contains(Properties.EYE) && targetState.contains(Properties.EYE)) { + return !currentState.get(Properties.EYE) && targetState.get(Properties.EYE); + } + + return false; + } + + @Override + protected @NotNull List getRequiredItems() { + return Collections.singletonList(new ItemStack(Items.ENDER_EYE)); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/FlowerPotFillGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/FlowerPotFillGuide.java new file mode 100644 index 000000000..108be2f42 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/FlowerPotFillGuide.java @@ -0,0 +1,40 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.Block; +import net.minecraft.block.FlowerPotBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class FlowerPotFillGuide extends InteractionGuide { + private final Block content; + + public FlowerPotFillGuide(SchematicBlockState state) { + super(state); + + Block targetBlock = state.targetState.getBlock(); + if (targetBlock instanceof FlowerPotBlock) { + this.content = ((FlowerPotBlock) targetBlock).getContent(); + } else { + this.content = null; + } + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (content == null) return false; + if (!(currentState.getBlock() instanceof FlowerPotBlock)) return false; + + return super.canExecute(player); + } + + @Override + protected @NotNull List getRequiredItems() { + if (content == null) return Collections.emptyList(); + else return Collections.singletonList(new ItemStack(content)); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/InteractionGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/InteractionGuide.java new file mode 100644 index 000000000..18b029bec --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/InteractionGuide.java @@ -0,0 +1,54 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.actions.ActionChain; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.actions.Action; +import me.aleksilassila.litematica.printer.v1_21_4.actions.PrepareAction; +import me.aleksilassila.litematica.printer.v1_21_4.actions.ReleaseShiftAction; +import me.aleksilassila.litematica.printer.v1_21_4.guides.Guide; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.actions.InteractActionImpl; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * A guide that clicks the current block to change its state. + */ +public abstract class InteractionGuide extends Guide { + public InteractionGuide(SchematicBlockState state) { + super(state); + } + + @Override + public @NotNull List execute(ClientPlayerEntity player) { + List actions = new ArrayList<>(); + + BlockHitResult hitResult = new BlockHitResult(Vec3d.ofCenter(state.blockPos), Direction.UP, state.blockPos, false); + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + int requiredSlot = getRequiredItemStackSlot(player); + + if (requiredSlot == -1) return actions; + + PrinterPlacementContext ctx = new PrinterPlacementContext(player, hitResult, requiredItem, requiredSlot); + + ActionChain chain = new ActionChain(); + + chain.addImmediateAction(new ReleaseShiftAction()); + chain.addImmediateAction(new PrepareAction(ctx)); + chain.addImmediateAction(new InteractActionImpl(ctx)); + + actions.add(chain); + + return actions; + } + + @Override + abstract protected @NotNull List getRequiredItems(); +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/LightCandleGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/LightCandleGuide.java new file mode 100644 index 000000000..e3bbc3122 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/LightCandleGuide.java @@ -0,0 +1,36 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.AbstractCandleBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.state.property.Properties; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class LightCandleGuide extends InteractionGuide { + boolean shouldBeLit; + boolean isLit; + + public LightCandleGuide(SchematicBlockState state) { + super(state); + + shouldBeLit = getProperty(targetState, Properties.LIT).orElse(false); + isLit = getProperty(currentState, Properties.LIT).orElse(false); + } + + @Override + protected @NotNull List getRequiredItems() { + return Collections.singletonList(new ItemStack(Items.FLINT_AND_STEEL)); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + + return (currentState.getBlock() instanceof AbstractCandleBlock) && shouldBeLit && !isLit; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/LogStrippingGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/LogStrippingGuide.java new file mode 100644 index 000000000..0f9f56748 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/LogStrippingGuide.java @@ -0,0 +1,47 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.mixin.AxeItemAccessor; +import net.minecraft.block.Block; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class LogStrippingGuide extends InteractionGuide { + static final Item[] AXE_ITEMS = new Item[]{ + Items.NETHERITE_AXE, + Items.DIAMOND_AXE, + Items.GOLDEN_AXE, + Items.IRON_AXE, + Items.STONE_AXE, + Items.WOODEN_AXE + }; + + public static final Map STRIPPED_BLOCKS = AxeItemAccessor.getStrippedBlocks(); + + public LogStrippingGuide(SchematicBlockState state) { + super(state); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!LitematicaMixinMod.STRIP_LOGS.getBooleanValue()) return false; + + if (!super.canExecute(player)) return false; + + Block strippingResult = STRIPPED_BLOCKS.get(currentState.getBlock()); + return strippingResult == targetState.getBlock(); + } + + @Override + protected @NotNull List getRequiredItems() { + return Arrays.stream(AXE_ITEMS).map(ItemStack::new).toList(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/TillingGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/TillingGuide.java new file mode 100644 index 000000000..a2d2a99c8 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/TillingGuide.java @@ -0,0 +1,39 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.guides.placement.FarmlandGuide; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +public class TillingGuide extends InteractionGuide { + public static final Item[] HOE_ITEMS = new Item[]{ + Items.NETHERITE_HOE, + Items.DIAMOND_HOE, + Items.GOLDEN_HOE, + Items.IRON_HOE, + Items.STONE_HOE, + Items.WOODEN_HOE + }; + + public TillingGuide(SchematicBlockState state) { + super(state); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + + return Arrays.stream(FarmlandGuide.TILLABLE_BLOCKS).anyMatch(b -> b == currentState.getBlock()); + } + + @Override + protected @NotNull List getRequiredItems() { + return Arrays.stream(HOE_ITEMS).map(ItemStack::new).toList(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/WaterLogGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/WaterLogGuide.java new file mode 100644 index 000000000..fb82287ab --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/interaction/WaterLogGuide.java @@ -0,0 +1,138 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.interaction; + + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.actions.*; +import me.aleksilassila.litematica.printer.v1_21_4.guides.placement.GeneralPlacementGuide; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.actions.UseItemActionImpl; +import net.minecraft.block.Waterloggable; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.state.property.Properties; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.RaycastContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/* + * @author IceTank + * @since 08.03.2026 + */ +public class WaterLogGuide extends GeneralPlacementGuide { + boolean canWork = false; + + public WaterLogGuide(SchematicBlockState state) { + super(state); + canWork = state.targetState.getBlock() instanceof Waterloggable; + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!canWork) return false; + if (!(currentState.getBlock() instanceof Waterloggable)) return false; + + if (currentState.get(Properties.WATERLOGGED) == null + || currentState.get(Properties.WATERLOGGED) == targetState.get(Properties.WATERLOGGED)) { + return false; + } + +// if (!super.canExecute(player)) return false; + List requiredItems = getRequiredItems(); + if (requiredItems.isEmpty() || requiredItems.stream().allMatch(i -> i.isOf(Items.AIR))) + return false; + for (Direction side : getPossibleSides()) { + if (canSeeBlockFace(player, new BlockHitResult(Vec3d.ofCenter(state.blockPos), side.getOpposite(), state.blockPos.offset(side), false))) { + return true; + } + } + return false; + } + + @Override + protected @NotNull List getRequiredItems() { + if (!canWork) return Collections.emptyList(); + return Collections.singletonList(new ItemStack(Items.WATER_BUCKET)); + } + + @Override + public @Nullable PrinterPlacementContext getPlacementContext(ClientPlayerEntity player) { + ClientWorld world = MinecraftClient.getInstance().world; + if (world == null) return null; + + Vec3d[] hitVecsToTryArray = getPossibleHitVecs(); + Vec3d playerEyePos = player.getEyePos(); + + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + int slot = getRequiredItemStackSlot(player); + + if (slot == -1) return null; + + for (Direction side : getPossibleSides()) { + Vec3d hitVec = Vec3d.ofCenter(state.blockPos) + .add(Vec3d.of(side.getVector()).multiply(0.5)); // Center of the block side face we are placing on + + for (Vec3d hitVecToTry : hitVecsToTryArray) { + Vec3d multiplier = Vec3d.of(side.getVector()); + multiplier = new Vec3d( + multiplier.x == 0 ? 1 : 0, + multiplier.y == 0 ? 1 : 0, + multiplier.z == 0 ? 1 : 0); // Offset from the Center of the block side face we are placing on by pre calculated values. This samples different points on that face. + + Vec3d blockHit = hitVec.add(hitVecToTry.multiply(multiplier)); + Vec3d lookDirection = blockHit.subtract(playerEyePos).normalize(); + Direction relativeDirection = Direction.getFacing(lookDirection.x, lookDirection.y, lookDirection.z); + + if (playerEyePos.distanceTo(blockHit) > LitematicaMixinMod.PRINTING_RANGE.getDoubleValue()) // Check if the hit vector is in range + continue; + + Vec3d lookVec = blockHit.subtract(playerEyePos).normalize(); // Look vector from the player's eye to the block hit vector + Vec3d raycastEnd = playerEyePos.add(lookVec.multiply(5)); // 5 block max distance + RaycastContext raycastContext = new RaycastContext(playerEyePos, raycastEnd, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.NONE, player); + BlockHitResult result = world.raycast(raycastContext); + if (result.getType() != HitResult.Type.BLOCK || !result.getBlockPos().equals(state.blockPos)) { // If we didn't hit a block, skip + continue; + } + if (result.getPos().distanceTo(playerEyePos) > LitematicaMixinMod.PRINTING_RANGE.getDoubleValue()) { // Check if the hit result is in range + continue; + } + + PrinterPlacementContext rayTraceContext = new PrinterPlacementContext(player, result, requiredItem, slot, relativeDirection, false); + rayTraceContext.canStealth = true; + rayTraceContext.isRaytrace = true; + return rayTraceContext; + } + } + return null; + } + + @Override + public @NotNull List execute(ClientPlayerEntity player) { + List actions = new ArrayList<>(); + + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + if (requiredItem.isEmpty()) return actions; + var ctx = getPlacementContext(player); + if (ctx == null) return actions; + + ActionChain chain = new ActionChain(); + chain.addImmediateAction(new PrepareLook(ctx)); + chain.addNextTickAction(new PrepareAction(ctx)); + chain.addNextTickAction(new UseItemActionImpl(ctx)); + + actions.add(chain); + + return actions; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/BlockIndifferentGuesserGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/BlockIndifferentGuesserGuide.java new file mode 100644 index 000000000..4835f6d4c --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/BlockIndifferentGuesserGuide.java @@ -0,0 +1,54 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.block.*; + +/** + * A GuesserGuide that ignores certain block state properties for specific blocks. + */ +public class BlockIndifferentGuesserGuide extends GeneralPlacementGuide { + public BlockIndifferentGuesserGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected boolean statesEqual(BlockState resultState, BlockState targetState) { + Block targetBlock = targetState.getBlock(); + Block resultBlock = resultState.getBlock(); + + if (targetBlock instanceof BambooBlock) { + return resultBlock instanceof BambooBlock || resultBlock instanceof BambooShootBlock; + } + + if (targetBlock instanceof BigDripleafStemBlock) { + if (resultBlock instanceof BigDripleafBlock || resultBlock instanceof BigDripleafStemBlock) { + return resultState.get(HorizontalFacingBlock.FACING) == targetState.get(HorizontalFacingBlock.FACING); + } + } + + if (targetBlock instanceof TwistingVinesPlantBlock) { + if (resultBlock instanceof TwistingVinesBlock) { + return true; + } else if (resultBlock instanceof TwistingVinesPlantBlock) { + return statesEqualIgnoreProperties(resultState, targetState, TwistingVinesBlock.AGE); + } + } + + if (targetBlock instanceof TripwireBlock && resultBlock instanceof TripwireBlock) { + return statesEqualIgnoreProperties(resultState, targetState, + TripwireBlock.ATTACHED, TripwireBlock.DISARMED, TripwireBlock.POWERED, TripwireBlock.NORTH, + TripwireBlock.EAST, TripwireBlock.SOUTH, TripwireBlock.WEST); + } + + if (targetBlock instanceof MushroomBlock) { + return statesEqualIgnoreProperties(resultState, targetState, MushroomBlock.DOWN, MushroomBlock.UP, MushroomBlock.WEST, MushroomBlock.NORTH, MushroomBlock.EAST, MushroomBlock.SOUTH); + } + + if (targetBlock instanceof GlowLichenBlock && PrinterConfig.PRINTER_ALLOW_NONE_EXACT_STATES.getBooleanValue()) { + return resultState.getBlock() == targetState.getBlock(); + } + + return super.statesEqual(resultState, targetState); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/BlockReplacementGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/BlockReplacementGuide.java new file mode 100644 index 000000000..caeae57b6 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/BlockReplacementGuide.java @@ -0,0 +1,75 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.guides.Guide; +import net.minecraft.block.CandleBlock; +import net.minecraft.block.SeaPickleBlock; +import net.minecraft.block.SlabBlock; +import net.minecraft.block.SnowBlock; +import net.minecraft.block.enums.SlabType; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.state.property.IntProperty; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; + +public class BlockReplacementGuide extends PlacementGuide { + private static final HashMap increasingProperties = new HashMap<>(); + + static { + increasingProperties.put(SnowBlock.LAYERS, null); + increasingProperties.put(SeaPickleBlock.PICKLES, null); + increasingProperties.put(CandleBlock.CANDLES, null); + } + + private Integer currentLevel = null; + private Integer targetLevel = null; + private IntProperty increasingProperty = null; + + public BlockReplacementGuide(SchematicBlockState state) { + super(state); + + for (IntProperty property : increasingProperties.keySet()) { + if (targetState.contains(property) && currentState.contains(property)) { + currentLevel = currentState.get(property); + targetLevel = targetState.get(property); + increasingProperty = property; + break; + } + } + } + + @Override + protected boolean getUseShift(SchematicBlockState state) { + return false; + } + + @Override + public @Nullable PrinterPlacementContext getPlacementContext(ClientPlayerEntity player) { + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + int slot = getRequiredItemStackSlot(player); + if (requiredItem.isEmpty() || slot == -1) return null; + + BlockHitResult hitResult = new BlockHitResult(Vec3d.ofCenter(state.blockPos), Direction.UP, state.blockPos, false); + return new PrinterPlacementContext(player, hitResult, requiredItem, slot); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (Guide.getProperty(targetState, SlabBlock.TYPE).orElse(null) == SlabType.DOUBLE && Guide.getProperty(currentState, SlabBlock.TYPE).orElse(SlabType.DOUBLE) != SlabType.DOUBLE) { + return super.canExecute(player); + } + + if (currentLevel == null || targetLevel == null || increasingProperty == null) return false; + if (!statesEqualIgnoreProperties(currentState, targetState, CandleBlock.LIT, increasingProperty)) return false; + if (currentLevel >= targetLevel) return false; + + return super.canExecute(player); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/ChestGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/ChestGuide.java new file mode 100644 index 000000000..88e636792 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/ChestGuide.java @@ -0,0 +1,92 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.BlockState; +import net.minecraft.block.ChestBlock; +import net.minecraft.block.enums.ChestType; +import net.minecraft.util.math.Direction; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Whilst making this guide, I learned that chests are much like humans. + * Some prefer to stay single, and some want to connect with another of its kind. + * Also that reversing chest connection logic is an enormous pain in the ass. I spent way too long on this. + * Thanks for coming to my ted talk + */ +public class ChestGuide extends GeneralPlacementGuide { + public ChestGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected boolean getRequiresExplicitShift() { + return true; + } + + @Override + public boolean skipOtherGuides() { + return true; + } + + @Override + protected Optional getLookDirection() { + return getProperty(targetState, ChestBlock.FACING) + .flatMap(facing -> Optional.of(facing.getOpposite())); + } + + @Override + protected List getPossibleSides() { + ChestType targetType = getProperty(targetState, ChestBlock.CHEST_TYPE).orElse(null); + Direction targetFacing = getProperty(targetState, ChestBlock.FACING).orElse(null); + + List sides = new ArrayList<>(); + + if (targetFacing == null || targetType == null) return sides; + + for (Direction direction : Direction.values()) { + if (targetType == ChestType.SINGLE && !willConnectToSide(state, direction)) { + sides.add(direction); + } else if (wantsToConnectToSide(state, direction) && willConnectToSide(state, direction)) { // :D + sides.add(direction); + } + } + + // Place single chests if cannot connect any existing chests + if (sides.isEmpty()) { + for (Direction direction : Direction.values()) { + if (!wantsToConnectToSide(state, direction) && !willConnectToSide(state, direction)) { + sides.add(direction); + } + } + } + + return sides; + } + + private boolean willConnectToSide(SchematicBlockState state, Direction neighborDirection) { + BlockState neighbor = state.offset(neighborDirection).currentState; + ChestType neighborType = getProperty(neighbor, ChestBlock.CHEST_TYPE).orElse(null); + Direction neighborFacing = getProperty(neighbor, ChestBlock.FACING).orElse(null); + Direction facing = getProperty(state.targetState, ChestBlock.FACING).orElse(null); + + if (neighborType == null || neighborFacing == null || facing == null) return false; + + if (facing.getAxis() == neighborDirection.getAxis() || neighborDirection.getAxis() == Direction.Axis.Y) + return false; + + return neighborType == ChestType.SINGLE && neighborFacing == facing && state.targetState.getBlock() == neighbor.getBlock(); + } + + private boolean wantsToConnectToSide(SchematicBlockState state, Direction direction) { + ChestType type = getProperty(state.targetState, ChestBlock.CHEST_TYPE).orElse(null); + Direction facing = getProperty(state.targetState, ChestBlock.FACING).orElse(null); + if (type == null || facing == null || type == ChestType.SINGLE) return false; + + Direction neighborDirection = type == ChestType.LEFT ? facing.rotateYClockwise() : facing.rotateYCounterclockwise(); + + return direction == neighborDirection; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FacingBlockGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FacingBlockGuide.java new file mode 100644 index 000000000..8901f6f05 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FacingBlockGuide.java @@ -0,0 +1,51 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.*; +import net.minecraft.block.enums.BlockHalf; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.Direction; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class FacingBlockGuide extends SlabGuide { + public FacingBlockGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected List getPossibleSides() { + Block block = state.targetState.getBlock(); + if (block instanceof WallSkullBlock || block instanceof WallSignBlock || block instanceof WallBannerBlock) { + Optional side = getProperty(state.targetState, Properties.HORIZONTAL_FACING).map(Direction::getOpposite); + return side.map(Collections::singletonList).orElseGet(Collections::emptyList); + } + if (block instanceof StairsBlock) { + Direction half = getRequiredHalf(state).getOpposite(); + return Arrays.stream(Direction.values()).filter(d -> d != half).toList(); + } + + return Arrays.stream(Direction.values()).toList(); + } + + @Override + public boolean skipOtherGuides() { + return true; + } + + private Direction getRequiredHalf(SchematicBlockState state) { + BlockState targetState = state.targetState; + BlockState currentState = state.currentState; + + if (!currentState.contains(StairsBlock.HALF)) { + return targetState.get(StairsBlock.HALF) == BlockHalf.TOP ? Direction.UP : Direction.DOWN; + } else if (currentState.get(StairsBlock.HALF) != targetState.get(StairsBlock.HALF)) { + return currentState.get(StairsBlock.HALF) == BlockHalf.TOP ? Direction.DOWN : Direction.UP; + } else { + return Direction.DOWN; + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FallingBlockGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FallingBlockGuide.java new file mode 100644 index 000000000..07f4b6785 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FallingBlockGuide.java @@ -0,0 +1,37 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.BlockState; +import net.minecraft.block.FallingBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.util.math.Direction; + +public class FallingBlockGuide extends GeneralPlacementGuide { + + public FallingBlockGuide(SchematicBlockState state) { + super(state); + } + + boolean blockPlacement() { + if (targetState.getBlock() instanceof FallingBlock) { + BlockState below = state.world.getBlockState(state.blockPos.offset(Direction.DOWN)); + return FallingBlock.canFallThrough(below); + } + + return false; + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (blockPlacement()) return false; + + return super.canExecute(player); + } + + @Override + public boolean skipOtherGuides() { + if (blockPlacement()) return true; + + return super.skipOtherGuides(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FarmlandGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FarmlandGuide.java new file mode 100644 index 000000000..d46eb6c21 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FarmlandGuide.java @@ -0,0 +1,29 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +public class FarmlandGuide extends GeneralPlacementGuide { + public static final Block[] TILLABLE_BLOCKS = new Block[]{ + Blocks.DIRT, + Blocks.GRASS_BLOCK, + Blocks.COARSE_DIRT, + Blocks.ROOTED_DIRT, + Blocks.DIRT_PATH, + }; + + public FarmlandGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected @NotNull List getRequiredItems() { + return Arrays.stream(TILLABLE_BLOCKS).map(b -> getBlockItem(b.getDefaultState())).toList(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FlowerPotGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FlowerPotGuide.java new file mode 100644 index 000000000..b793daf38 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/FlowerPotGuide.java @@ -0,0 +1,20 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class FlowerPotGuide extends GeneralPlacementGuide { + public FlowerPotGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected @NotNull List getRequiredItems() { + return Collections.singletonList(new ItemStack(Items.FLOWER_POT)); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/GeneralPlacementGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/GeneralPlacementGuide.java new file mode 100644 index 000000000..d67fc39b6 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/GeneralPlacementGuide.java @@ -0,0 +1,433 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import fi.dy.masa.litematica.world.SchematicWorldHandler; +import fi.dy.masa.litematica.world.WorldSchematic; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.block.*; +import net.minecraft.block.enums.ChestType; +import net.minecraft.block.enums.SlabType; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.state.property.Properties; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.RaycastContext; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * An old school guide where there are defined specific conditions + * for player state depending on the block being placed. + */ +public class GeneralPlacementGuide extends PlacementGuide { + protected static Vec3d[] extendedHitVecs = new Vec3d[]{ + new Vec3d(-0.25, -0.25, -0.25), + new Vec3d(+0.25, -0.25, -0.25), + new Vec3d(-0.25, +0.25, -0.25), + new Vec3d(-0.25, -0.25, +0.25), + new Vec3d(+0.25, +0.25, -0.25), + new Vec3d(-0.25, +0.25, +0.25), + new Vec3d(+0.25, -0.25, +0.25), + new Vec3d(+0.25, +0.25, +0.25), + new Vec3d(-0.25, -0.49, -0.25), // 1/4 Just above the lower edge of a block. For carpets for instance as they are very thin + new Vec3d(+0.25, -0.49, -0.25), // 2/4 + new Vec3d(-0.25, -0.49, +0.25), // 3/4 + new Vec3d(+0.25, -0.49, +0.25) // 4/4 + }; + private PrinterPlacementContext contextCache = null; + + public GeneralPlacementGuide(SchematicBlockState state) { + super(state); + } + + protected List getPossibleSides() { + return Arrays.asList(Direction.values()); + } + + protected Optional getLookDirection() { + return Optional.empty(); + } + + protected boolean getRequiresSupport() { + return false; + } + + protected boolean getRequiresExplicitShift() { + return false; + } + + protected Vec3d getHitModifier(Direction validSide) { + return new Vec3d(0, 0, 0); + } + + private Optional getValidSide(SchematicBlockState state) { + boolean printInAir = false; // LitematicaMixinMod.PRINT_IN_AIR.getBooleanValue(); + + List sides = getPossibleSides(); + + if (sides.isEmpty()) { + return Optional.empty(); + } + + List validSides = new ArrayList<>(); + for (Direction side : sides) { + if (printInAir && !getRequiresSupport()) { + return Optional.of(side); + } else { + SchematicBlockState neighborState = state.offset(side); + + if (getProperty(neighborState.currentState, SlabBlock.TYPE).orElse(null) == SlabType.DOUBLE) { + validSides.add(side); + continue; + } + + if (canBeClicked(neighborState.world, neighborState.blockPos) && // Handle unclickable grass for example + !neighborState.currentState.isReplaceable()) + validSides.add(side); + } + } + + for (Direction validSide : validSides) { + if (!isInteractive(state.offset(validSide).currentState.getBlock())) { + return Optional.of(validSide); + } + } + + return validSides.isEmpty() ? Optional.empty() : Optional.of(validSides.get(0)); + } + + protected Vec3d[] getPossibleHitVecs() { + return extendedHitVecs; + } + + private List getValidSides(SchematicBlockState state) { + List sides = getPossibleSides(); + + List validSides = new ArrayList<>(); + for (Direction side : sides) { + SchematicBlockState neighborState = state.offset(side); + + if (getProperty(neighborState.currentState, SlabBlock.TYPE).orElse(null) == SlabType.DOUBLE) { + validSides.add(side); + continue; + } + + if (canBeClicked(neighborState.world, neighborState.blockPos) && // Handle unclickable grass for example + !neighborState.currentState.isReplaceable()) + validSides.add(side); + } + + return validSides; + } + + protected boolean getUseShift(SchematicBlockState state) { + if (getRequiresExplicitShift()) return true; + + Direction clickSide = getValidSide(state).orElse(null); + if (clickSide == null) return false; + return isInteractive(state.offset(clickSide).currentState.getBlock()); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + if (!PrinterConfig.STRICT_BLOCK_FACE_CHECK.getBooleanValue()) { + return true; + } + for (Direction side : getPossibleSides()) { + if (canSeeBlockFace(player, new BlockHitResult(Vec3d.ofCenter(state.blockPos), side.getOpposite(), state.blockPos.offset(side), false))) { + return true; + } + } + return false; + } + + @Override + public @Nullable PrinterPlacementContext getPlacementContext(ClientPlayerEntity player) { + if (PrinterConfig.PRINTER_AIRPLACE.getBooleanValue()) { + return getAirplaceContext(player); + } else { + return getContextByStrictLook(player); + } + } + + @Nullable + public PrinterPlacementContext getContextByStrictLook(ClientPlayerEntity player) { + if (contextCache != null && !LitematicaMixinMod.DEBUG && !PrinterConfig.NO_PLACEMENT_CACHE.getBooleanValue() && contextCache.isRaytrace == PrinterConfig.RAYCAST.getBooleanValue()) + return contextCache; + + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + int slot = getRequiredItemStackSlot(player); + + if (slot == -1) return null; + + Vec3d[] hitVecsToTryArray = getPossibleHitVecs(); + + Vec3d playerEyePos = player.getEyePos(); + // Direction relativeRotation = Direction.getEntityFacingOrder(player)[0]; + + // for (Direction lookDirection : directionsToTry) { + for (Direction side : getPossibleSides()) { + BlockPos neighborPos = state.blockPos.offset(side); + + // Check if the block face is visible. Prevents the printer from trying to place blocks on the backside of other blocks + if (PrinterConfig.STRICT_BLOCK_FACE_CHECK.getBooleanValue()) { + if (!canSeeBlockFace(player, new BlockHitResult(Vec3d.ofCenter(state.blockPos), side.getOpposite(), neighborPos, false))) { + continue; + } + } + + BlockState neighborState = state.world.getBlockState(neighborPos); + boolean requiresShift = getRequiresExplicitShift() || isInteractive(neighborState.getBlock()); + + if (!canBeClicked(state.world, neighborPos) || // Handle unclickable grass for example + neighborState.isReplaceable()) + continue; + + Vec3d hitVec = Vec3d.ofCenter(state.blockPos) + .add(Vec3d.of(side.getVector()).multiply(0.5)); // Center of the block side face we are placing on + + // Now we bring on the big guns, brute force the hit vector until we find a solution that directly hits the neighbor block without obstruction + for (Vec3d hitVecToTry : hitVecsToTryArray) { + Vec3d multiplier = Vec3d.of(side.getVector()); + multiplier = new Vec3d( + multiplier.x == 0 ? 1 : 0, + multiplier.y == 0 ? 1 : 0, + multiplier.z == 0 ? 1 : 0); // Offset from the Center of the block side face we are placing on by pre calculated values. This samples different points on that face. + + Vec3d blockHit = hitVec.add(hitVecToTry.multiply(multiplier)); + Vec3d lookDirection = blockHit.subtract(playerEyePos).normalize(); + Direction relativeDirection = Direction.getFacing(lookDirection.x, lookDirection.y, lookDirection.z); + + if (playerEyePos.distanceTo(blockHit) > LitematicaMixinMod.PRINTING_RANGE.getDoubleValue()) // Check if the hit vector is in range + continue; + + if (PrinterConfig.RAYCAST.getBooleanValue() && mc.world != null && mc.player != null) { + Vec3d lookVec = blockHit.subtract(playerEyePos).normalize(); // Look vector from the player's eye to the block hit vector + Vec3d raycastEnd = playerEyePos.add(lookVec.multiply(5)); // 5 block max distance + RaycastContext raycastContext = new RaycastContext(playerEyePos, raycastEnd, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.NONE, player); + BlockHitResult result = mc.world.raycast(raycastContext); + if (result.getType() != HitResult.Type.BLOCK) { // If we didn't hit a block, skip + continue; + } + + if (result.getBlockPos().equals(neighborPos)) { + if (PrinterConfig.RAYCAST_STRICT_BLOCK_HIT.getBooleanValue()) { // Check if the right side was hit + Direction hitSide = result.getSide(); + if (hitSide.getOpposite() != side) { + continue; + } + } + if (result.getPos().distanceTo(playerEyePos) > LitematicaMixinMod.PRINTING_RANGE.getDoubleValue()) { // Check if the hit result is in range + continue; + } + BlockHitResult hitResult = new BlockHitResult(blockHit, side.getOpposite(), neighborPos, false); + PrinterPlacementContext rayTraceContext = new PrinterPlacementContext(player, hitResult, requiredItem, slot, relativeDirection, requiresShift); + rayTraceContext.canStealth = true; + rayTraceContext.isRaytrace = true; + BlockState resultState = getRequiredItemAsBlock(player) + .orElse(targetState.getBlock()) + .getPlacementState(rayTraceContext); + + if (resultState != null && correctObserverPlacement(targetState, resultState) && (statesEqual(resultState, targetState) || correctChestPlacement(targetState, resultState))) { + contextCache = rayTraceContext; + return rayTraceContext; + } + } + continue; + } else /* No Raycast */ { + BlockHitResult hitResult = new BlockHitResult(blockHit, side.getOpposite(), neighborPos, false); + PrinterPlacementContext context = new PrinterPlacementContext(player, hitResult, requiredItem, slot, relativeDirection, requiresShift); + context.canStealth = true; + BlockState result = getRequiredItemAsBlock(player) + .orElse(targetState.getBlock()) + .getPlacementState(context); // FIXME torch shift clicks another torch and getPlacementState is the clicked block, which is true + + if (result != null && correctObserverPlacement(targetState, result) && (statesEqual(result, targetState) || correctChestPlacement(targetState, result))) { + contextCache = context; + return context; + } + } + } + } + // } + + return null; + } + + protected static Direction[] directionsToTry = new Direction[]{ + Direction.NORTH, + Direction.SOUTH, + Direction.EAST, + Direction.WEST, + Direction.UP, + Direction.DOWN + }; + + /** + * Produces a PrinterPlacementContext for air placing blocks. The HitResult is brute forced by trying all sides and + * various hit positions inside the block volume. The HitResult block places against the neighbor block in the given + * side direction. To get the target block position we offset the {@link BlockHitResult#getBlockPos()} by the side + * direction. + * @param player the player + * @return the PrinterPlacementContext or null if no valid context was found + */ + @Nullable + public PrinterPlacementContext getAirplaceContext(ClientPlayerEntity player) { + if (contextCache != null && !LitematicaMixinMod.DEBUG) return contextCache; + + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + int slot = getRequiredItemStackSlot(player); + + if (slot == -1) return null; + + if (Printer.inactivityCounter <= PrinterConfig.PRINTER_MIN_INACTIVE_TIME_AIR_PLACE.getIntegerValue()) { + return null; + } + + // We'll brute force: sides (including horizontal) + hit positions inside the target block volume + // to emulate realistic clicks that yield correct orientation for special blocks (hoppers, froglights, basalt, slabs). + + // First quick attempt: original center (UP side) (fast path) + { + BlockHitResult hr = new BlockHitResult(Vec3d.ofCenter(state.blockPos), Direction.UP, state.blockPos, true); + PrinterPlacementContext quick = new PrinterPlacementContext(player, hr, requiredItem, slot, null, false); + BlockState res = getRequiredItemAsBlock(player).orElse(targetState.getBlock()).getPlacementState(quick); + if (res != null && correctObserverPlacement(targetState, res) && statesEqual(res, targetState)) { + contextCache = quick; + quick.isAirPlace = true; + return quick; + } + } + + // Hit offsets inside block relative to its min corner (0..1). Include lower y (<0.5) for bottom slabs, and center variations. + double[] ySamples = new double[]{0.25, 0.5, 0.75}; + double[] xzSamples = new double[]{0.25, 0.5, 0.75}; + + for (Direction lookDirection : directionsToTry) { + for (Direction side : getPossibleSides()) { + // For each sample point on the face or interior adjust the hit vector. + for (double y : ySamples) { + for (double x : xzSamples) { + for (double z : xzSamples) { + // Start with raw interior sample + Vec3d sample = new Vec3d(x, y, z); + Vec3d hitVec = Vec3d.of(state.blockPos).add(sample); + + // Ensure the hitVec lies on the correct face for the side: project coordinate component to face plane center + hitVec = switch (side) { + case UP -> new Vec3d(hitVec.x, state.blockPos.getY() + 1 - 1e-4, hitVec.z); + case DOWN -> new Vec3d(hitVec.x, state.blockPos.getY() + 1e-4, hitVec.z); + case NORTH -> new Vec3d(hitVec.x, hitVec.y, state.blockPos.getZ() + 1e-4); + case SOUTH -> new Vec3d(hitVec.x, hitVec.y, state.blockPos.getZ() + 1 - 1e-4); + case WEST -> new Vec3d(state.blockPos.getX() + 1e-4, hitVec.y, hitVec.z); + case EAST -> new Vec3d(state.blockPos.getX() + 1 - 1e-4, hitVec.y, hitVec.z); + }; + + BlockHitResult hitResult = new BlockHitResult(hitVec, side.getOpposite(), state.blockPos, true); + PrinterPlacementContext context = new PrinterPlacementContext(player, hitResult, requiredItem, slot, lookDirection, false); + BlockState result = getRequiredItemAsBlock(player) + .orElse(targetState.getBlock()) + .getPlacementState(context); + if (result != null && correctObserverPlacement(targetState, result) && statesEqual(result, targetState)) { + contextCache = context; + context.isAirPlace = true; + return context; + } + } + } + } + } + } + +// if (PrinterConfig.PRINTER_AIRPLACE_FLOATING_ONLY.getBooleanValue() && !isInAir(state.blockPos)) { +// return null; +// } +// if (Printer.inactivityCounter < PrinterConfig.PRINTER_MIN_INACTIVE_TIME_AIR_PLACE.getIntegerValue()) { +// return null; +// } +// if (PrinterConfig.PRINTER_AIRPLACE_RANGE.getDoubleValue() < 0 || player.getEyePos().distanceTo(Vec3d.ofCenter(state.blockPos)) > PrinterConfig.PRINTER_AIRPLACE_RANGE.getDoubleValue()) { +// return null; +// } + + return null; + } + + private boolean correctChestPlacement(BlockState targetState, BlockState result) { + if (targetState.contains(ChestBlock.CHEST_TYPE) && result.contains(ChestBlock.CHEST_TYPE) && result.get(ChestBlock.FACING) == targetState.get(ChestBlock.FACING)) { + ChestType targetChestType = targetState.get(ChestBlock.CHEST_TYPE); + ChestType resultChestType = result.get(ChestBlock.CHEST_TYPE); + + return targetChestType != ChestType.SINGLE && resultChestType == ChestType.SINGLE; + } + + return false; + } + + /** + * Returns true if the observer is placed correctly or if the config printerPlaceObserversLast is set to false + * @param targetState the target state of the block being placed + * @param result the result state of the block being placed + * @return true if the observer is placed correctly, false otherwise + */ + private boolean correctObserverPlacement(BlockState targetState, BlockState result) { + if (!PrinterConfig.PRINTER_PLACE_OBSERVERS_LAST.getBooleanValue()) return true; // If the config is set to place observers last, we don't need to check this + + if (result.getBlock() != Blocks.OBSERVER) return true; + if (targetState.getBlock() != Blocks.OBSERVER) return true; + + Direction facing = result.get(Properties.FACING); + if (facing == null) return false; + + WorldSchematic schematicWorld = SchematicWorldHandler.getSchematicWorld(); + if (schematicWorld == null) return false; + if (mc.world == null) return false; + + if (schematicWorld.getBlockState(state.blockPos.offset(facing)).isAir()) { + return true; + } else { + return !mc.world.getBlockState(state.blockPos.offset(facing)).isAir(); + } + } + + protected boolean canSeeBlockFace(ClientPlayerEntity player, BlockHitResult hitResult) { + // Draw a line between the player pos and the block pos and check if the block side is visible + // BlockPos targetPos = state.blockPos; + + Direction side = hitResult.getSide(); + BlockPos blockPos = hitResult.getBlockPos(); + Vec3d playerEyePos = player.getEyePos(); + switch (side) { + case UP: + if (blockPos.getY() + 1 > playerEyePos.getY()) return false; + break; + case DOWN: + if (blockPos.getY() < playerEyePos.getY()) return false; + break; + case NORTH: + if (blockPos.getZ() < playerEyePos.getZ()) return false; + break; + case SOUTH: + if (blockPos.getZ() + 1 > playerEyePos.getZ()) return false; + break; + case EAST: + if (blockPos.getX() + 1 > playerEyePos.getX()) return false; + break; + case WEST: + if (blockPos.getX() < playerEyePos.getX()) return false; + break; + } + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/LogGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/LogGuide.java new file mode 100644 index 000000000..d1d29900d --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/LogGuide.java @@ -0,0 +1,54 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.guides.interaction.LogStrippingGuide; +import net.minecraft.block.Block; +import net.minecraft.block.PillarBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.Direction; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class LogGuide extends GeneralPlacementGuide { + public LogGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected List getPossibleSides() { + if (targetState.contains(PillarBlock.AXIS)) { + Direction.Axis axis = targetState.get(PillarBlock.AXIS); + return Arrays.stream(Direction.values()).filter(d -> d.getAxis() == axis).toList(); + } + + return new ArrayList<>(); + } + + @Override + protected @NotNull List getRequiredItems() { + for (Block log : LogStrippingGuide.STRIPPED_BLOCKS.keySet()) { + if (targetState.getBlock() == LogStrippingGuide.STRIPPED_BLOCKS.get(log)) { + return Collections.singletonList(new ItemStack(log)); + } + } + + return super.getRequiredItems(); + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!LitematicaMixinMod.STRIP_LOGS.getBooleanValue()) return false; + + if (LogStrippingGuide.STRIPPED_BLOCKS.containsValue(targetState.getBlock())) { + return super.canExecute(player); + } + + return false; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/PlacementGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/PlacementGuide.java new file mode 100644 index 000000000..bf80b4b4e --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/PlacementGuide.java @@ -0,0 +1,193 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.actions.*; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.guides.Guide; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.actions.AirPlaceAction; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.actions.InteractActionImpl; +import net.minecraft.block.*; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.BlockItem; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.util.function.BooleanBiFunction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Guide that clicks its neighbors to create a placement in target position. + */ +abstract public class PlacementGuide extends Guide { + MinecraftClient mc = MinecraftClient.getInstance(); + + public PlacementGuide(SchematicBlockState state) { + super(state); + } + + protected ItemStack getBlockItem(BlockState state) { + return state.getBlock().asItem().getDefaultStack(); + } + + protected Optional getRequiredItemAsBlock(ClientPlayerEntity player) { + ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); + + if (requiredItem.isEmpty()) { + return Optional.empty(); + } else { + if (requiredItem.getItem() instanceof BlockItem) + return Optional.of(((BlockItem) requiredItem.getItem()).getBlock()); + else return Optional.empty(); + } + } + + @Override + protected @NotNull List getRequiredItems() { + return Collections.singletonList(getBlockItem(state.targetState)); + } + + abstract protected boolean getUseShift(SchematicBlockState state); + + @Nullable + abstract public PrinterPlacementContext getPlacementContext(ClientPlayerEntity player); + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + + if (targetState.getBlock() == currentState.getBlock()) { + return false; + } + + List requiredItems = getRequiredItems(); + if (requiredItems.isEmpty() || requiredItems.stream().allMatch(i -> i.isOf(Items.AIR))) + return false; + + ItemPlacementContext ctx = getPlacementContext(player); + if (ctx == null || !ctx.canPlace()) return false; + + BlockState resultState = getRequiredItemAsBlock(player) + .orElse(targetState.getBlock()) + .getPlacementState(ctx); + + if (resultState != null) { + if (collidesWithPlayer(resultState)) { + if (PrinterConfig.isDebug()) { + System.out.println("Block collides with player. Not placing."); + } + return false; + } + if (!resultState.canPlaceAt(state.world, state.blockPos)) return false; + return !(currentState.getBlock() instanceof FluidBlock) || canPlaceInWater(resultState); + } else { + return false; + } + + } + + public boolean isInAir(BlockPos pos) { + if (mc.world == null) return false; + for (Direction dir : Direction.values()) { + if (!mc.world.getBlockState(pos.offset(dir)).isAir()) { + return false; + } + } + return true; + } + + private boolean collidesWithPlayer(BlockState blockState) { + if (mc.player == null || mc.world == null) return true; + + VoxelShape shape = blockState.getCollisionShape(state.schematic, state.blockPos); + if (shape.isEmpty()) return false; + shape = shape.offset(state.blockPos.getX(), state.blockPos.getY(), state.blockPos.getZ()); + Box playerShape = mc.player.getBoundingBox(); + return VoxelShapes.matchesAnywhere(VoxelShapes.cuboid(playerShape), shape, BooleanBiFunction.AND); + } + + @Override + public @NotNull List execute(ClientPlayerEntity player) { + List actions = new ArrayList<>(); + PrinterPlacementContext ctx = getPlacementContext(player); + + if (ctx == null) return actions; + ActionChain actionChain = new ActionChain(); + + if (ctx.isAirPlace) { + if (ctx.lookDirection != null) { + if (PrinterConfig.PRINTER_GRIM_ROTATION.getBooleanValue()) { + actionChain.addNextTickAction(new PrepareLook(ctx)); + } else { + actionChain.addImmediateAction(new PrepareLook(ctx)); + } + } + actionChain.addNextTickAction(new PrepareAction(ctx)); + actionChain.addNextTickAction(new AirPlaceAction(ctx)); + actions.add(actionChain); + return actions; + } else { + if (PrinterConfig.PRINTER_AIRPLACE.getBooleanValue() && PrinterConfig.PRINTER_AIRPLACE_ONLY.getBooleanValue()) { + return actions; + } + } + + boolean shiftDown = mc.player.input.playerInput.sneak(); + actionChain.addImmediateAction(new PrepareLook(ctx)); + if (ctx.shouldSneak && !shiftDown) actionChain.addImmediateAction(new PresShift()); + actionChain.addNextTickAction(new PrepareAction(ctx)); + actionChain.addNextTickAction(new InteractActionImpl(ctx)); + if (ctx.shouldSneak && !shiftDown) actionChain.addNextTickAction(new ReleaseShiftAction()); + actions.add(actionChain); + + return actions; + } + + protected static boolean canBeClicked(World world, BlockPos pos) { + return getOutlineShape(world, pos) != VoxelShapes.empty() && !(world.getBlockState(pos).getBlock() instanceof AbstractSignBlock); // FIXME signs + } + + private static VoxelShape getOutlineShape(World world, BlockPos pos) { + return world.getBlockState(pos).getOutlineShape(world, pos); + } + + public boolean isInteractive(Block block) { + for (Class clazz : interactiveBlocks) { + if (clazz.isInstance(block)) { + return true; + } + } + + return false; + } + + private boolean canPlaceInWater(BlockState blockState) { + Block block = blockState.getBlock(); + if (block instanceof FluidFillable) { + return true; + } else if (!(block instanceof DoorBlock) && !(blockState.getBlock() instanceof AbstractSignBlock) && !blockState.isOf(Blocks.LADDER) && !blockState.isOf(Blocks.SUGAR_CANE) && !blockState.isOf(Blocks.BUBBLE_COLUMN)) { +// Material material = blockState.getMaterial(); +// if (material != Material.PORTAL && material != Material.STRUCTURE_VOID && material != Material.UNDERWATER_PLANT && material != Material.REPLACEABLE_UNDERWATER_PLANT) { +// return material.blocksMovement(); +// } else { +// return true; +// } + return blockState.blocksMovement(); + } + + return true; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/PropertySpecificGuesserGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/PropertySpecificGuesserGuide.java new file mode 100644 index 000000000..318812382 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/PropertySpecificGuesserGuide.java @@ -0,0 +1,75 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.block.*; +import net.minecraft.state.property.Properties; +import net.minecraft.state.property.Property; +import java.util.stream.Stream; + +public class PropertySpecificGuesserGuide extends GeneralPlacementGuide { + protected static Property[] ignoredProperties = new Property[]{ + RepeaterBlock.DELAY, + ComparatorBlock.MODE, + ComposterBlock.LEVEL, + RedstoneWireBlock.POWER, + RedstoneWireBlock.WIRE_CONNECTION_EAST, + RedstoneWireBlock.WIRE_CONNECTION_NORTH, + RedstoneWireBlock.WIRE_CONNECTION_SOUTH, + RedstoneWireBlock.WIRE_CONNECTION_WEST, + Properties.POWERED, + Properties.TRIGGERED, + Properties.OPEN, + PointedDripstoneBlock.THICKNESS, + ScaffoldingBlock.DISTANCE, + ScaffoldingBlock.BOTTOM, + CactusBlock.AGE, + BambooBlock.AGE, + BambooBlock.LEAVES, + BambooBlock.STAGE, + SaplingBlock.STAGE, + HorizontalConnectingBlock.EAST, + HorizontalConnectingBlock.NORTH, + HorizontalConnectingBlock.SOUTH, + HorizontalConnectingBlock.WEST, + SnowBlock.LAYERS, + SeaPickleBlock.PICKLES, + CandleBlock.CANDLES, + EndPortalFrameBlock.EYE, + Properties.LIT, + LeavesBlock.DISTANCE, + LeavesBlock.PERSISTENT, + Properties.ATTACHED, + Properties.NOTE, + Properties.INSTRUMENT, + Properties.EXTENDED, + Properties.WEST_WALL_SHAPE, + Properties.EAST_WALL_SHAPE, + Properties.NORTH_WALL_SHAPE, + Properties.SOUTH_WALL_SHAPE, + Properties.ENABLED + }; + + public static Property[] rotationProperties = new Property[]{ + Properties.ROTATION, + Properties.HORIZONTAL_FACING, + Properties.AXIS, + Properties.HORIZONTAL_AXIS + }; + + public PropertySpecificGuesserGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected boolean statesEqual(BlockState resultState, BlockState targetState) { + if (PrinterConfig.PRINTER_IGNORE_ROTATION.getBooleanValue()) { + // Combine rotation properties with ignored properties + return statesEqualIgnoreProperties(resultState, targetState, Stream.concat( + Stream.of(rotationProperties), + Stream.of(ignoredProperties) + ).toArray(Property[]::new)); + } + return statesEqualIgnoreProperties(resultState, targetState, ignoredProperties); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/RailGuesserGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/RailGuesserGuide.java new file mode 100644 index 000000000..255d82fcd --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/RailGuesserGuide.java @@ -0,0 +1,128 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.BlockState; +import net.minecraft.block.enums.RailShape; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.Direction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class RailGuesserGuide extends GeneralPlacementGuide { + static final RailShape[] STRAIGHT_RAIL_SHAPES = new RailShape[]{ + RailShape.NORTH_SOUTH, + RailShape.EAST_WEST + }; + + public RailGuesserGuide(SchematicBlockState state) { + super(state); + } + + @Override + public boolean skipOtherGuides() { + return true; + } + + @Override + protected boolean statesEqual(BlockState resultState, BlockState targetState) { + if (!wouldConnectCorrectly()) return false; +// if (wouldBlockAnotherConnection()) return false; + /*TODO: Fully working rail guesser + * If has a neighbor that: + * - Has not been placed yet + * - OR Has been placed but can change shape + * - AND this placement should connect to only one rail, that is not the neighbor + * Then return false + * */ + + if (getRailShape(resultState).isPresent()) { + if (Arrays.stream(STRAIGHT_RAIL_SHAPES).anyMatch(shape -> shape == getRailShape(resultState).orElse(null))) { + return super.statesEqualIgnoreProperties(resultState, targetState, Properties.RAIL_SHAPE, Properties.STRAIGHT_RAIL_SHAPE, Properties.POWERED); + } + } + + return super.statesEqual(resultState, targetState); + } + + private boolean wouldConnectCorrectly() { + RailShape targetShape = getRailShape(state.targetState).orElse(null); + if (targetShape == null) return false; + + List allowedConnections = getRailDirections(targetShape); + + List possibleConnections = new ArrayList<>(); + for (Direction d : Direction.values()) { + if (d.getAxis().isVertical()) continue; + SchematicBlockState neighbor = state.offset(d); + + if (hasFreeConnections(neighbor)) { + possibleConnections.add(d); + } + } + + if (possibleConnections.size() > 2) return false; + + return allowedConnections.containsAll(possibleConnections); + } + +// private boolean wouldBlockAnotherConnection() { +// List possibleConnections = new ArrayList<>(); +// +// for (Direction d : Direction.values()) { +// if (d.getAxis().isVertical()) continue; +// SchematicBlockState neighbor = state.offset(d); +// +// if (couldConnectWrongly(neighbor)) { +// possibleConnections.add(d); +// } +// } +// +// return possibleConnections.size() > 1; +// } + + private boolean hasFreeConnections(SchematicBlockState state) { + List possibleConnections = getRailDirections(state); + if (possibleConnections.isEmpty()) return false; + + for (Direction d : possibleConnections) { + SchematicBlockState neighbor = state.offset(d); + if (neighbor.currentState.getBlock() != neighbor.currentState.getBlock()) { + return false; + } + } + + return possibleConnections.stream().anyMatch(possibleDirection -> { + SchematicBlockState neighbor = state.offset(possibleDirection); + return !getRailDirections(neighbor).contains(possibleDirection.getOpposite()); + }); + } + + private List getRailDirections(SchematicBlockState state) { + RailShape shape = getRailShape(state.currentState).orElse(null); + if (shape == null) return new ArrayList<>(); + + return getRailDirections(shape); + } + + private List getRailDirections(RailShape railShape) { + String name = railShape.getName(); + + if (railShape.isAscending()) { + Direction d = Direction.valueOf(name.replace("ascending_", "").toUpperCase()); + return Arrays.asList(d, d.getOpposite()); + } else { + Direction d1 = Direction.valueOf(name.split("_")[0].toUpperCase()); + Direction d2 = Direction.valueOf(name.split("_")[1].toUpperCase()); + return Arrays.asList(d1, d2); + } + } + + Optional getRailShape(BlockState state) { + Optional shape = getProperty(state, Properties.RAIL_SHAPE); + if (shape.isEmpty()) return getProperty(state, Properties.STRAIGHT_RAIL_SHAPE); + return shape; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/RotatingBlockGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/RotatingBlockGuide.java new file mode 100644 index 000000000..fd0f97b0c --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/RotatingBlockGuide.java @@ -0,0 +1,68 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.*; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RotationCalculator; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class RotatingBlockGuide extends GeneralPlacementGuide { + public RotatingBlockGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected List getPossibleSides() { + Block block = state.targetState.getBlock(); + if (block instanceof WallSkullBlock || block instanceof WallSignBlock || block instanceof WallBannerBlock) { + Optional side = getProperty(state.targetState, Properties.HORIZONTAL_FACING).map(Direction::getOpposite); + return side.map(Collections::singletonList).orElseGet(Collections::emptyList); + } + + return Collections.singletonList(Direction.DOWN); + } + + @Override + public boolean skipOtherGuides() { + return true; + } + + @Override + public boolean canExecute(ClientPlayerEntity player) { + if (!super.canExecute(player)) return false; + + if (PrinterConfig.PRINTER_IGNORE_ROTATION.getBooleanValue()) return true; + + int rotation = getProperty(state.targetState, Properties.ROTATION).orElse(0); + if (targetState.getBlock() instanceof BannerBlock || targetState.getBlock() instanceof SignBlock) { + rotation = (rotation + 8) % 16; + } + int distTo0 = rotation > 8 ? 16 - rotation : rotation; + float yaw = Math.round(distTo0 / 8f * 180f * (rotation > 8 ? -1 : 1)); + + Direction targetDirection; + float g = yaw * 0.017453292F; + float j = MathHelper.sin(g); + float k = MathHelper.cos(g); + boolean bl = j > 0.0F; + boolean bl3 = k > 0.0F; + float l = bl ? j : -j; + float n = bl3 ? k : -k; + Direction direction = bl ? Direction.EAST : Direction.WEST; + Direction direction3 = bl3 ? Direction.SOUTH : Direction.NORTH; + if (l > n) { + targetDirection = direction; + } else { + targetDirection = direction3; + } + + return player.getHorizontalFacing() == targetDirection; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/SlabGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/SlabGuide.java new file mode 100644 index 000000000..ce07c02fd --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/SlabGuide.java @@ -0,0 +1,37 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.BlockState; +import net.minecraft.block.SlabBlock; +import net.minecraft.block.enums.SlabType; +import net.minecraft.util.math.Direction; + +import java.util.Arrays; +import java.util.List; + +public class SlabGuide extends GeneralPlacementGuide { + public SlabGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected List getPossibleSides() { + return Arrays.stream(Direction.values()) + .filter(d -> d != (getRequiredHalf(state).getOpposite()) && + getProperty(state.offset(d).currentState, SlabBlock.TYPE).orElse(SlabType.DOUBLE) == SlabType.DOUBLE) + .toList(); + } + + private Direction getRequiredHalf(SchematicBlockState state) { + BlockState targetState = state.targetState; + BlockState currentState = state.currentState; + + if (!currentState.contains(SlabBlock.TYPE)) { + return targetState.get(SlabBlock.TYPE) == SlabType.TOP ? Direction.UP : Direction.DOWN; + } else if (currentState.get(SlabBlock.TYPE) != targetState.get(SlabBlock.TYPE)) { + return currentState.get(SlabBlock.TYPE) == SlabType.TOP ? Direction.DOWN : Direction.UP; + } else { + return Direction.DOWN; + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/TorchGuide.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/TorchGuide.java new file mode 100644 index 000000000..57117279b --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/placement/TorchGuide.java @@ -0,0 +1,34 @@ +package me.aleksilassila.litematica.printer.v1_21_4.guides.placement; + +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import net.minecraft.block.Block; +import net.minecraft.block.HorizontalFacingBlock; +import net.minecraft.block.WallMountedBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.util.math.Direction; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class TorchGuide extends PropertySpecificGuesserGuide { + public TorchGuide(SchematicBlockState state) { + super(state); + } + + @Override + protected List getPossibleSides() { + // Prefer the wall-mounted FACING for wall torches; fallback to horizontal facing + Optional facing = getProperty(targetState, WallMountedBlock.FACING); + if (facing.isEmpty()) facing = getProperty(targetState, HorizontalFacingBlock.FACING); + + return facing + .map(direction -> Collections.singletonList(direction.getOpposite())) + .orElseGet(() -> Collections.singletonList(Direction.DOWN)); + } + + @Override + protected Optional getRequiredItemAsBlock(ClientPlayerEntity player) { + return Optional.of(state.targetState.getBlock()); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/BlockHelperImpl.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/BlockHelperImpl.java new file mode 100644 index 000000000..7c5749e32 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/BlockHelperImpl.java @@ -0,0 +1,14 @@ +package me.aleksilassila.litematica.printer.v1_21_4.implementation; + +import me.aleksilassila.litematica.printer.v1_21_4.BlockHelper; +import net.minecraft.block.ButtonBlock; + +import java.util.Arrays; + +public class BlockHelperImpl extends BlockHelper { + static { + interactiveBlocks.addAll(Arrays.asList( + ButtonBlock.class + )); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/PrinterPlacementContext.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/PrinterPlacementContext.java new file mode 100644 index 000000000..6bc0b1daa --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/PrinterPlacementContext.java @@ -0,0 +1,92 @@ +package me.aleksilassila.litematica.printer.v1_21_4.implementation; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.block.BlockState; +import net.minecraft.block.FireBlock; +import net.minecraft.block.FluidBlock; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.Direction; +import org.jetbrains.annotations.Nullable; + +public class PrinterPlacementContext extends ItemPlacementContext { + public final @Nullable Direction lookDirection; + public final boolean shouldSneak; + public final BlockHitResult hitResult; + public final int requiredItemSlot; + public boolean canStealth = false; + public boolean isRaytrace = false; + public boolean isAirPlace = false; + + public PrinterPlacementContext(PlayerEntity player, BlockHitResult hitResult, ItemStack requiredItem, int requiredItemSlot) { + this(player, hitResult, requiredItem, requiredItemSlot, null, false); + } + + public PrinterPlacementContext(PlayerEntity player, BlockHitResult hitResult, ItemStack requiredItem, int requiredItemSlot, @Nullable Direction lookDirection, boolean requiresSneaking) { + super(player, Hand.MAIN_HAND, requiredItem, hitResult); + + this.lookDirection = lookDirection; + this.shouldSneak = requiresSneaking; + this.hitResult = hitResult; + this.requiredItemSlot = requiredItemSlot; + } + + @Override + public Direction getPlayerLookDirection() { + return lookDirection == null ? super.getPlayerLookDirection() : lookDirection; + } + + @Override + public Direction getVerticalPlayerLookDirection() { + if (lookDirection != null && lookDirection.getOpposite() == super.getVerticalPlayerLookDirection()) + return lookDirection; + return super.getVerticalPlayerLookDirection(); + } + + @Override + public Direction getHorizontalPlayerFacing() { + if (lookDirection == null || !lookDirection.getAxis().isHorizontal()) return super.getHorizontalPlayerFacing(); + + return lookDirection; + } + + @Override + public boolean canPlace() { + if (!isAirPlace) { + return super.canPlace(); + } + if (!super.canPlace()) { + return false; + } + BlockState currentState = this.getWorld().getBlockState(hitResult.getBlockPos()); + if (this.getPlayer().getEyePos().distanceTo(hitResult.getBlockPos().toCenterPos()) > PrinterConfig.PRINTER_AIRPLACE_RANGE.getDoubleValue()) { + return false; + } + // Wrong state and not replaceable + if (!currentState.isReplaceable() && !(currentState.getBlock() instanceof FireBlock)) { + return false; + } else { + // Replaceable block. Check fluid source block replacement config + if (currentState.getBlock() instanceof FluidBlock && !LitematicaMixinMod.REPLACE_FLUIDS_SOURCE_BLOCKS.getBooleanValue()) { + // Only allow replacing fluid source blocks if the config is enabled + return false; + } + } + return true; + } + + @Override + public String toString() { + return "PrinterPlacementContext{" + + "lookDirection=" + lookDirection + + ", requiresSneaking=" + shouldSneak + + ", blockPos=" + hitResult.getBlockPos() + + ", side=" + hitResult.getSide() + +// ", hitVec=" + hitResult + + '}'; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/AirPlaceAction.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/AirPlaceAction.java new file mode 100644 index 000000000..5c6a186f5 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/AirPlaceAction.java @@ -0,0 +1,66 @@ +package me.aleksilassila.litematica.printer.v1_21_4.implementation.actions; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.actions.InteractAction; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.FluidBlock; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.BlockItem; +import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket; +import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; + +public class AirPlaceAction extends InteractAction { + private final MinecraftClient mc = MinecraftClient.getInstance(); + public AirPlaceAction(PrinterPlacementContext context) { + super(context); + } + + @Override + protected ActionResult interact(MinecraftClient client, ClientPlayerEntity player, Hand hand, BlockHitResult hitResult) { + BlockPos pos = hitResult.isInsideBlock() ? hitResult.getBlockPos() : hitResult.getBlockPos().offset(hitResult.getSide()); + + if (!context.canPlace()) { + return ActionResult.FAIL; + } + airPlace(pos); + return ActionResult.PASS; + } + + private void airPlace(BlockPos pos) { + ClientPlayNetworkHandler connection = mc.getNetworkHandler(); + ClientPlayerInteractionManager interactionManager = mc.interactionManager; + if (mc.player == null || connection == null || interactionManager == null) return; + + if (mc.isInSingleplayer() && mc.player.getAbilities().creativeMode) { + BlockHitResult blockHitResult = new BlockHitResult(this.context.hitResult.getPos(), this.context.hitResult.getSide(), pos, true); + mc.interactionManager.interactBlock(mc.player, Hand.MAIN_HAND, blockHitResult); + mc.player.swingHand(Hand.MAIN_HAND, false); + return; + } + + if (mc.player.getInventory().getStack(PlayerInventory.OFF_HAND_SLOT).getItem() instanceof BlockItem) { + mc.interactionManager.clickSlot(0, 45, 0, SlotActionType.PICKUP, mc.player); + mc.getNetworkHandler().sendPacket(new CloseHandledScreenC2SPacket(mc.player.playerScreenHandler.syncId)); + } + connection.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND, BlockPos.ORIGIN, Direction.DOWN)); + + Hand hand = Hand.OFF_HAND; + BlockHitResult blockHitResult = new BlockHitResult(this.context.hitResult.getPos(), this.context.hitResult.getSide(), pos, true); + interactionManager.interactBlock(mc.player, hand, blockHitResult); + mc.player.swingHand(Hand.MAIN_HAND, false); + connection.sendPacket(new HandSwingC2SPacket(hand)); + connection.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND, BlockPos.ORIGIN, Direction.DOWN)); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/InteractActionImpl.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/InteractActionImpl.java new file mode 100644 index 000000000..7324d75cc --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/InteractActionImpl.java @@ -0,0 +1,26 @@ +package me.aleksilassila.litematica.printer.v1_21_4.implementation.actions; + +import me.aleksilassila.litematica.printer.v1_21_4.actions.InteractAction; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; + +public class InteractActionImpl extends InteractAction { + public InteractActionImpl(PrinterPlacementContext context) { + super(context); + } + private final MinecraftClient mc = MinecraftClient.getInstance(); + @Override + protected ActionResult interact(MinecraftClient client, ClientPlayerEntity player, Hand hand, BlockHitResult hitResult) { + ActionResult result = client.interactionManager.interactBlock(player, hand, hitResult); + if (!result.isAccepted()) { + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) System.out.println("Failed to interact with block got " + result); + } + mc.player.swingHand(Hand.MAIN_HAND); + return result; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/UseItemActionImpl.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/UseItemActionImpl.java new file mode 100644 index 000000000..e838fbeab --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/actions/UseItemActionImpl.java @@ -0,0 +1,31 @@ +package me.aleksilassila.litematica.printer.v1_21_4.implementation.actions; + + +import me.aleksilassila.litematica.printer.v1_21_4.actions.InteractAction; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import me.aleksilassila.litematica.printer.v1_21_4.implementation.PrinterPlacementContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; + +/* + * @author IceTank + * @since 08.03.2026 + */ +public class UseItemActionImpl extends InteractAction { + public UseItemActionImpl(PrinterPlacementContext context) { + super(context); + } + private final MinecraftClient mc = MinecraftClient.getInstance(); + @Override + protected ActionResult interact(MinecraftClient client, ClientPlayerEntity player, Hand hand, BlockHitResult hitResult) { + ActionResult result = client.interactionManager.interactItem(player, hand); + if (!result.isAccepted()) { + if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) System.out.println("Failed to interact with block got " + result); + } + mc.player.swingHand(Hand.MAIN_HAND); + return result; + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/mixin/MixinClientPlayerEntity.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/mixin/MixinClientPlayerEntity.java new file mode 100644 index 000000000..da7390589 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/implementation/mixin/MixinClientPlayerEntity.java @@ -0,0 +1,109 @@ +package me.aleksilassila.litematica.printer.v1_21_4.implementation.mixin; + +import com.mojang.authlib.GameProfile; +import fi.dy.masa.litematica.world.SchematicWorldHandler; +import fi.dy.masa.litematica.world.WorldSchematic; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicBlockState; +import me.aleksilassila.litematica.printer.v1_21_4.UpdateChecker; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.network.packet.c2s.play.UpdateSignC2SPacket; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Optional; + +@Mixin(ClientPlayerEntity.class) +public class MixinClientPlayerEntity extends AbstractClientPlayerEntity { + + private static boolean didCheckForUpdates = false; + + @Shadow + protected MinecraftClient client; + @Shadow + public ClientPlayNetworkHandler networkHandler; + + public MixinClientPlayerEntity(ClientWorld world, GameProfile profile) { + super(world, profile); + } + + @Inject(at = @At("TAIL"), method = "tick") + public void tick(CallbackInfo ci) { + if (!didCheckForUpdates) { + didCheckForUpdates = true; + + checkForUpdates(); + } + + if (LitematicaMixinMod.freeLook.getPrevPerspective() == null) { + LitematicaMixinMod.freeLook.setPrevPerspective(client.options.getPerspective()); + } + if (LitematicaMixinMod.printer != null) { +// LitematicaMixinMod.printer.actionHandler.onPostTick(); + } + } + + @Inject(at = @At("HEAD"), method = "tick") + public void tickHead(CallbackInfo ci) { + ClientPlayerEntity clientPlayer = (ClientPlayerEntity) (Object) this; + if (LitematicaMixinMod.printer == null || LitematicaMixinMod.printer.player != clientPlayer) { + System.out.println("Initializing printer, player: " + clientPlayer + ", client: " + client); + LitematicaMixinMod.printer = new Printer(client, clientPlayer); + } + LitematicaMixinMod.printer.actionHandler.processPreviousTickActions(); + LitematicaMixinMod.printer.onGameTick(); + LitematicaMixinMod.printer.actionHandler.processCurrentTickActions(); + Printer.inventoryManager.tick(); + LitematicaMixinMod.freeLook.onGameTick(); + LitematicaMixinMod.movementHandler.onGameTick(); + } + + public void checkForUpdates() { + new Thread(() -> { + String version = UpdateChecker.version; + String newVersion = UpdateChecker.getPrinterVersion(); + + if (!version.equals(newVersion)) { + client.inGameHud.getChatHud().addMessage(Text.literal("New version of Litematica Printer available in https://github.com/aleksilassila/litematica-printer/releases")); + } + }).start(); + } + + @Inject(method = "openEditSignScreen", at = @At("HEAD"), cancellable = true) + public void openEditSignScreen(SignBlockEntity sign, boolean front, CallbackInfo ci) { + getTargetSignEntity(sign).ifPresent(signBlockEntity -> { + UpdateSignC2SPacket packet = new UpdateSignC2SPacket(sign.getPos(), + front, + signBlockEntity.getText(front).getMessage(0, false).getString(), + signBlockEntity.getText(front).getMessage(1, false).getString(), + signBlockEntity.getText(front).getMessage(2, false).getString(), + signBlockEntity.getText(front).getMessage(3, false).getString()); + this.networkHandler.sendPacket(packet); + ci.cancel(); + }); + } + + private Optional getTargetSignEntity(SignBlockEntity sign) { + WorldSchematic worldSchematic = SchematicWorldHandler.getSchematicWorld(); + SchematicBlockState state = new SchematicBlockState(sign.getWorld(), worldSchematic, sign.getPos()); + + BlockEntity targetBlockEntity = worldSchematic.getBlockEntity(state.blockPos); + + if (targetBlockEntity instanceof SignBlockEntity targetSignEntity) { + return Optional.of(targetSignEntity); + } + + return Optional.empty(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/AbstractBlockInvoker.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/AbstractBlockInvoker.java new file mode 100644 index 000000000..724848ab3 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/AbstractBlockInvoker.java @@ -0,0 +1,21 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +/* + * @author IceTank + * @since 17.03.2025 + */ + +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.BlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(AbstractBlock.class) +public interface AbstractBlockInvoker { + @Invoker("getPickStack") + ItemStack getPickStack(WorldView world, BlockPos pos, BlockState state, boolean includeData); +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/AxeItemAccessor.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/AxeItemAccessor.java new file mode 100644 index 000000000..0ab009401 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/AxeItemAccessor.java @@ -0,0 +1,20 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import net.minecraft.block.Block; +import net.minecraft.item.AxeItem; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +/** + * This class apparently fixes an issue with Quilt. + */ +@Mixin(AxeItem.class) +public interface AxeItemAccessor { + @Accessor("STRIPPED_BLOCKS") + static Map getStrippedBlocks() { + throw new AssertionError("Untransformed @Accessor"); + } + +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java new file mode 100644 index 000000000..07abd02e0 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java @@ -0,0 +1,41 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.malilib.gui.Message; +import fi.dy.masa.malilib.gui.button.ButtonBase; +import fi.dy.masa.malilib.gui.widgets.WidgetFileBrowserBase; +import fi.dy.masa.malilib.util.InfoUtils; +import me.aleksilassila.litematica.printer.v1_21_4.SchematicConverter; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +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; + +/** + * @author IceTank + * @since 17.12.2024 + */ +@Mixin(targets = "fi.dy.masa.litematica.gui.GuiSchematicLoad$ButtonListener", remap = false) +public class ButtonListenerMixin { + + @Inject(method = "actionPerformedWithButton", + at = @At( + target = "Lfi/dy/masa/litematica/data/SchematicHolder;addSchematic(Lfi/dy/masa/litematica/schematic/LitematicaSchematic;Z)V", + value = "INVOKE", shift = At.Shift.BEFORE) + ) + private void onActionPerformedWithButton(ButtonBase button, int mouseButton, CallbackInfo ci, + @Local LocalBooleanRef warnType, @Local LocalRef schematic, + @Local WidgetFileBrowserBase.DirectoryEntry entry) { + if (!warnType.get() || !PrinterConfig.AUTO_CONVERT_SCHEMATIC_TO_LITEMATIC_ON_LOAD.getBooleanValue()) { + return; + } + LitematicaSchematic newSchem = SchematicConverter.convertAndReturn(schematic.get().getFile().toFile(), entry.getDirectory().toFile()); + warnType.set(false); + schematic.set(newSchem); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, 15000, "Auto converted schematic to litematic format"); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ConfigsMixin.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ConfigsMixin.java new file mode 100644 index 000000000..4a2fce2bc --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ConfigsMixin.java @@ -0,0 +1,43 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import com.google.common.collect.ImmutableList; +import fi.dy.masa.litematica.config.Configs; +import fi.dy.masa.malilib.config.IConfigBase; +import fi.dy.masa.malilib.config.options.ConfigHotkey; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(value = Configs.class, remap = false) +public class ConfigsMixin { + @Redirect(method = "loadFromFile", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Configs$Generic;OPTIONS:Lcom/google/common/collect/ImmutableList;")) + private static ImmutableList moreOptions() { + return LitematicaMixinMod.getConfigList(); + } + + @Redirect(method = "saveToFile", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Configs$Generic;OPTIONS:Lcom/google/common/collect/ImmutableList;")) + private static ImmutableList moreeOptions() { + return LitematicaMixinMod.getConfigList(); + } + + @Redirect(method = "loadFromFile", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Hotkeys;HOTKEY_LIST:Ljava/util/List;")) + private static List moreHotkeys() { + return LitematicaMixinMod.getHotkeyList(); + } + + @Redirect(method = "saveToFile", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Hotkeys;HOTKEY_LIST:Ljava/util/List;")) + private static List moreeHotkeys() { + return LitematicaMixinMod.getHotkeyList(); + } + + @Inject(method = "loadFromFile", at = @At("RETURN")) + private static void loadFromFilePost(CallbackInfo ci) { + PrinterConfig.onConfigFileLoad(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/GuiConfigsMixin.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/GuiConfigsMixin.java new file mode 100644 index 000000000..f3251f46b --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/GuiConfigsMixin.java @@ -0,0 +1,61 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import com.google.common.collect.ImmutableList; +import fi.dy.masa.litematica.gui.GuiConfigs; +import fi.dy.masa.malilib.config.IConfigBase; +import fi.dy.masa.malilib.config.options.ConfigHotkey; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.List; + +@Mixin(value = GuiConfigs.class, remap = false) +public class GuiConfigsMixin { + + /*@Overwrite + public List getConfigs() + { + List configs; + ConfigGuiTab tab = DataManager.getConfigGuiTab(); + + if (tab == ConfigGuiTab.GENERIC) + { + configs = LitematicaMixinMod.betterConfigList; + } + else if (tab == ConfigGuiTab.INFO_OVERLAYS) + { + configs = Configs.InfoOverlays.OPTIONS; + } + else if (tab == ConfigGuiTab.VISUALS) + { + configs = Configs.Visuals.OPTIONS; + } + else if (tab == ConfigGuiTab.COLORS) + { + configs = Configs.Colors.OPTIONS; + } + else if (tab == ConfigGuiTab.HOTKEYS) + { + configs = LitematicaMixinMod.betterHotkeyList; + } + else + { + return Collections.emptyList(); + } + + return ConfigOptionWrapper.createFor(configs); + }*/ + + + @Redirect(method = "getConfigs", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Configs$Generic;OPTIONS:Lcom/google/common/collect/ImmutableList;")) + private ImmutableList moreOptions() { + return LitematicaMixinMod.getConfigList(); + } + + @Redirect(method = "getConfigs", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Hotkeys;HOTKEY_LIST:Ljava/util/List;")) + private List moreHotkeys() { + return LitematicaMixinMod.getHotkeyList(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/InputHandlerMixin.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/InputHandlerMixin.java new file mode 100644 index 000000000..e59f708c3 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/InputHandlerMixin.java @@ -0,0 +1,25 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import fi.dy.masa.litematica.event.InputHandler; +import fi.dy.masa.malilib.config.options.ConfigHotkey; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.List; + +@Mixin(value = InputHandler.class, remap = false) +public class InputHandlerMixin { + + @Redirect(method = "addHotkeys", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Hotkeys;HOTKEY_LIST:Ljava/util/List;")) + private List moreHotkeys() { + return LitematicaMixinMod.getHotkeyList(); + } + + @Redirect(method = "addKeysToMap", at = @At(value = "FIELD", target = "Lfi/dy/masa/litematica/config/Hotkeys;HOTKEY_LIST:Ljava/util/List;")) + private List moreeHotkeys() { + return LitematicaMixinMod.getHotkeyList(); + } + +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/LitematicaSchematicAccessor.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/LitematicaSchematicAccessor.java new file mode 100644 index 000000000..5e23cc0f2 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/LitematicaSchematicAccessor.java @@ -0,0 +1,20 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.litematica.util.FileType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.nio.file.Path; + +/** + * @author IceTank + * @since 17.12.2024 + */ +@Mixin(LitematicaSchematic.class) +public interface LitematicaSchematicAccessor { + @Invoker("") + static LitematicaSchematic invokeConstructor(Path file, FileType fileType) { + throw new AssertionError(); + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinAccessorClientPlayerEntity.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinAccessorClientPlayerEntity.java new file mode 100644 index 000000000..7a0a21e68 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinAccessorClientPlayerEntity.java @@ -0,0 +1,11 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import net.minecraft.client.network.ClientPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ClientPlayerEntity.class) +public interface MixinAccessorClientPlayerEntity { + @Accessor("ticksLeftToDoubleTapSprint") + void setTicksLeftToDoubleTapSprint(int ticksLeftToDoubleTapSprint); +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinAccessorKeyBinding.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinAccessorKeyBinding.java new file mode 100644 index 000000000..5fdeae18b --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinAccessorKeyBinding.java @@ -0,0 +1,12 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(KeyBinding.class) +public interface MixinAccessorKeyBinding { + @Accessor("boundKey") + InputUtil.Key getBoundKey(); +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinCamera.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinCamera.java new file mode 100644 index 000000000..1e0e84f6d --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinCamera.java @@ -0,0 +1,22 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import me.aleksilassila.litematica.printer.v1_21_4.FreeLook; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import net.minecraft.client.render.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Mixin(Camera.class) +public class MixinCamera { + @ModifyArgs(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V")) + private void onUpdateSetRotationArgs(Args args) { + FreeLook freeLook = LitematicaMixinMod.freeLook; + + if (freeLook.isEnabled()) { + args.set(0, freeLook.getCameraYaw()); + args.set(1, freeLook.getCameraPitch()); + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinClientConnection.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinClientConnection.java new file mode 100644 index 000000000..fe9f02e53 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinClientConnection.java @@ -0,0 +1,69 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import io.netty.channel.ChannelHandlerContext; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.BlockItem; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.screen.PlayerScreenHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientConnection.class) +public class MixinClientConnection { + @Unique + private static final MinecraftClient mc = MinecraftClient.getInstance(); + + @Inject(method = "channelRead0*", at = @At("HEAD"), cancellable = true) + private void channelReadPre(ChannelHandlerContext channelHandlerContext, Packet packet, CallbackInfo callback) { + if (!LitematicaMixinMod.PRINT_MODE.getBooleanValue() && !LitematicaMixinMod.PRINT.getKeybind().isPressed()) { + return; + } + if (Printer.inactivityCounter > 20) { + return; + } + if (PrinterConfig.PRINTER_AIRPLACE.getBooleanValue() && PrinterConfig.PRINTER_AIRPLACE_OFFHAND_SLOT_SUPPRESS.getBooleanValue()) { + if (packet instanceof ScreenHandlerSlotUpdateS2CPacket packet1) { + if (packet1.getSyncId() == -2 && packet1.getSlot() == PlayerInventory.OFF_HAND_SLOT) { + callback.cancel(); + } else if (packet1.getSyncId() == 0 && PlayerScreenHandler.isInHotbar(packet1.getSyncId())) { + if (packet1.getSlot() == PlayerScreenHandler.OFFHAND_ID) { + callback.cancel(); + } + } + } + } + // This prevents the server from sending the client the packets that would normally be sent to the client + // Just wanted to keep this comment above because copilot wrote it. What does that even mean? + // This prevents 2b from fucking up your inventory with useless slot update packets. Turns out 2b sends slot update + // packet AND an entire inventory update packet just because. The client does not like that so you get a shit ton of ghost items. + if (PrinterConfig.PRINTER_SUPER_CHINESE_GHOST_ITEM_FIX.getBooleanValue()) { + if (packet instanceof ScreenHandlerSlotUpdateS2CPacket packet1) { + // Looks like faulty packet is always on syncId = 0 + if (packet1.getSyncId() == 0 && mc.player != null) { + if (packet1.getSlot() >= PlayerScreenHandler.HOTBAR_START || packet1.getSlot() < PlayerScreenHandler.HOTBAR_END) { + net.minecraft.item.ItemStack current = mc.player.playerScreenHandler.getSlot(packet1.getSlot()).getStack(); + net.minecraft.item.ItemStack incoming = packet1.getStack(); + // Only cancel if it's a redundant duplicate (same item AND same count). + // Allow through if the server is correcting the count — blocking those + // causes the client to think it still has blocks it already placed, + // which results in ghost block placements. + if (current.getItem() instanceof BlockItem + && net.minecraft.item.ItemStack.areItemsAndComponentsEqual(current, incoming) + && current.getCount() == incoming.getCount()) { + callback.cancel(); + } + } + } + } + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinClientPlayerEntity.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinClientPlayerEntity.java new file mode 100644 index 000000000..998ee467e --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinClientPlayerEntity.java @@ -0,0 +1,19 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import me.aleksilassila.litematica.printer.v1_21_4.config.PrinterConfig; +import net.minecraft.client.network.ClientPlayerEntity; +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(ClientPlayerEntity.class) +public class MixinClientPlayerEntity { + @Inject(method = "tickMovement", at = @At("HEAD")) + public void tickMovement(CallbackInfo ci) { + MixinClientPlayerEntity clientPlayer = this; + if (PrinterConfig.PREVENT_DOUBLE_TAP_SPRINTING.getBooleanValue()) { + ((MixinAccessorClientPlayerEntity) clientPlayer).setTicksLeftToDoubleTapSprint(0); + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java new file mode 100644 index 000000000..bc0a9f3e5 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java @@ -0,0 +1,57 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import me.aleksilassila.litematica.printer.v1_21_4.FreeLook; +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.Mouse; +import net.minecraft.util.math.MathHelper; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = Mouse.class) +public class MixinMouse { + @Shadow private double cursorDeltaX; + @Shadow private double cursorDeltaY; + + @Shadow @Final private MinecraftClient client; + @Inject(method = "updateMouse", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/MinecraftClient;getTutorialManager()Lnet/minecraft/client/tutorial/TutorialManager;", + shift = At.Shift.BEFORE + ), cancellable = true + ) + private void updateMouseChangeLookDirection(CallbackInfo ci) { + FreeLook freeLook = LitematicaMixinMod.freeLook; + if (freeLook.isEnabled()) { + double f = this.client.options.getMouseSensitivity().getValue() * 0.6000000238418579 + 0.20000000298023224; + double g = f * f * f; + double h = g * 8.0; + double k = this.cursorDeltaX * h; + double l = this.cursorDeltaY * h; + int m = 1; + if (this.client.options.getInvertMouseY().getValue()) { + m = -1; + } + float yaw = (float) (freeLook.getCameraYaw() + k * 0.15F); + freeLook.setCameraYaw(yaw); + float pitch = MathHelper.clamp((float) (freeLook.getCameraPitch() + (l * (double) m) * 0.15F), -90.0F, 90.0F); + freeLook.setCameraPitch(pitch); + if (Math.abs(pitch) > 90.0F) { + yaw = pitch > 0.0F ? 90.0F : -90.0F; + freeLook.setCameraYaw(yaw); + } + cursorDeltaX = 0.0; + cursorDeltaY = 0.0; + if (freeLook.shouldRotate()) { + if (this.client.player != null) { + this.client.player.changeLookDirection(k, l * (double)m); + } + } + ci.cancel(); + } + } +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerInventoryAccessor.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerInventoryAccessor.java new file mode 100644 index 000000000..47a91b413 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerInventoryAccessor.java @@ -0,0 +1,11 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import net.minecraft.entity.player.PlayerInventory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(PlayerInventory.class) +public interface PlayerInventoryAccessor { + @Accessor("selectedSlot") + void setSelectedSlot(int slot); +} diff --git a/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerMoveC2SPacketMixin.java b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerMoveC2SPacketMixin.java new file mode 100644 index 000000000..96f394145 --- /dev/null +++ b/v1_21_11/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerMoveC2SPacketMixin.java @@ -0,0 +1,36 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod; +import me.aleksilassila.litematica.printer.v1_21_4.Printer; +import me.aleksilassila.litematica.printer.v1_21_4.actions.PrepareAction; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(PlayerMoveC2SPacket.class) +public class PlayerMoveC2SPacketMixin { + @ModifyVariable(method = "", at = @At("HEAD"), ordinal = 0, argsOnly = true) + private static float modifyLookYaw(float yaw) { + Printer printer = LitematicaMixinMod.printer; + if (printer == null) return yaw; + + PrepareAction action = printer.actionHandler.lookAction; + if (action != null && action.modifyYaw) { + if (LitematicaMixinMod.DEBUG) System.out.println("YAW: " + action.yaw); + return action.yaw; + } else return yaw; + } + + @ModifyVariable(method = "", at = @At("HEAD"), ordinal = 1, argsOnly = true) + private static float modifyLookPitch(float pitch) { + Printer printer = LitematicaMixinMod.printer; + if (printer == null) return pitch; + + PrepareAction action = printer.actionHandler.lookAction; + if (action != null && action.modifyPitch) { + if (LitematicaMixinMod.DEBUG) System.out.println("PITCH: " + action.pitch); + return action.pitch; + } else return pitch; + } +} diff --git a/v1_21_11/src/main/resources/assets/modid/icon.png b/v1_21_11/src/main/resources/assets/modid/icon.png new file mode 100644 index 000000000..8a087cc63 Binary files /dev/null and b/v1_21_11/src/main/resources/assets/modid/icon.png differ diff --git a/v1_21_11/src/main/resources/fabric.mod.json b/v1_21_11/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..411f0f947 --- /dev/null +++ b/v1_21_11/src/main/resources/fabric.mod.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "id": "litematica_printer", + "version": "${version}", + "name": "Litematica Printer", + "description": "A fork of Litematica that adds the missing printer functionality", + "authors": [ + "aleksilassila" + ], + "contact": { + "homepage": "https://github.com/aleksilassila/litematica-printer", + "sources": "https://github.com/aleksilassila/litematica-printer" + }, + "license": "CC0-1.0", + "icon": "assets/modid/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "me.aleksilassila.litematica.printer.v1_21_4.LitematicaMixinMod" + ] + }, + "mixins": [ + "litematica-printer.mixins.json", + "litematica-printer-implementation.mixins.json" + ], + "depends": { + "fabricloader": ">=0.18.1", + "fabric": "*", + "minecraft": "~1.21.11", + "java": ">=21" + }, + "custom": { + "modmenu": { + "parent": "carpet" + } + } +} + diff --git a/v1_21_11/src/main/resources/litematica-printer-implementation.mixins.json b/v1_21_11/src/main/resources/litematica-printer-implementation.mixins.json new file mode 100644 index 000000000..3262866da --- /dev/null +++ b/v1_21_11/src/main/resources/litematica-printer-implementation.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "me.aleksilassila.litematica.printer.v1_21_4.implementation.mixin", + "compatibilityLevel": "JAVA_16", + "mixins": [ + ], + "client": [ + "MixinClientPlayerEntity" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/v1_21_11/src/main/resources/litematica-printer.mixins.json b/v1_21_11/src/main/resources/litematica-printer.mixins.json new file mode 100644 index 000000000..327b3b52b --- /dev/null +++ b/v1_21_11/src/main/resources/litematica-printer.mixins.json @@ -0,0 +1,27 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "me.aleksilassila.litematica.printer.v1_21_4.mixin", + "compatibilityLevel": "JAVA_16", + "mixins": [ + "LitematicaSchematicAccessor", + "MixinClientConnection" + ], + "client": [ + "AxeItemAccessor", + "ButtonListenerMixin", + "ConfigsMixin", + "GuiConfigsMixin", + "InputHandlerMixin", + "MixinAccessorClientPlayerEntity", + "MixinAccessorKeyBinding", + "PlayerInventoryAccessor", + "MixinCamera", + "MixinClientPlayerEntity", + "MixinMouse", + "PlayerMoveC2SPacketMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java index 4be0539c7..a9a227786 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/InventoryManager.java @@ -108,7 +108,7 @@ private boolean swapToHotbar(ClientPlayerEntity player, Item item) { } hotbarSlots.get(nextSlot).addTicksLocked(10); hotbarSlots.get(nextSlot).waitingForItem = item; - player.getInventory().selectedSlot = nextSlot; + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(nextSlot); if (PrinterConfig.PRINTER_DEBUG_LOG.getBooleanValue()) { System.out.println("Swapping item from inventory: " + slot + " into hotbar -> " + nextSlot); } @@ -127,7 +127,7 @@ public void pickSlot(WorldSchematic world, ClientPlayerEntity player, BlockPos p int slot = inv.getSlotWithStack(stack); boolean shouldPick = slot > 8; if (slot != -1 && !shouldPick) { - player.getInventory().selectedSlot = slot; + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(slot); } else if (slot != -1) { InventoryUtils.setPickedItemToHand(slot, stack, mc); // https://github.com/sakura-ryoko/litematica-printer/blob/f8e38a2b31708e61f8a5fad0f2989d6834495da4/src/main/java/me/aleksilassila/litematica/printer/actions/PrepareAction.java#L71 } else if (Configs.Generic.PICK_BLOCK_SHULKERS.getBooleanValue()) { @@ -136,7 +136,7 @@ public void pickSlot(WorldSchematic world, ClientPlayerEntity player, BlockPos p if (slot > 8) { InventoryUtils.setPickedItemToHand(slot, stack, mc); } else { - inv.selectedSlot = slot; + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) inv).setSelectedSlot(slot); } } } @@ -154,7 +154,7 @@ public static int findSlotWithBoxWithItem(PlayerInventory inventory, ItemStack s int bestCount = lestFirst ? Integer.MAX_VALUE : 0; int bestSlot = -1; - for (int slotNum = 0; slotNum < inventory.main.size(); slotNum += 1) { + for (int slotNum = 0; slotNum < 36; slotNum += 1) { ItemStack itemStack = inventory.getStack(slotNum); int count = shulkerBoxItemCount(itemStack, stackReference); if (lestFirst && count < bestCount && count > 0) { @@ -197,8 +197,8 @@ public boolean select(ItemStack itemStack) { if (player.getAbilities().creativeMode) { depositCursorStack(); this.addPickBlock(inventory, itemStack); - mc.interactionManager.clickCreativeStack(player.getStackInHand(Hand.MAIN_HAND), 36 + inventory.selectedSlot); - updateLastUsedSlot(inventory.selectedSlot); + mc.interactionManager.clickCreativeStack(player.getStackInHand(Hand.MAIN_HAND), 36 + inventory.getSelectedSlot()); + updateLastUsedSlot(inventory.getSelectedSlot()); return true; } else { int hotbarSlot = getHotbarSlotWithItem(player, itemStack); @@ -214,9 +214,9 @@ public boolean select(ItemStack itemStack) { if (hotbarSlot == -1) { return false; } - if (hotbarSlot != player.getInventory().selectedSlot) { + if (hotbarSlot != player.getInventory().getSelectedSlot()) { player.networkHandler.sendPacket(new UpdateSelectedSlotC2SPacket(hotbarSlot)); - player.getInventory().selectedSlot = hotbarSlot; + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(hotbarSlot); } updateLastUsedSlot(hotbarSlot); return false; @@ -225,9 +225,9 @@ public boolean select(ItemStack itemStack) { } else { depositCursorStack(); // Switch to hotbar slot - if (hotbarSlot != player.getInventory().selectedSlot) { + if (hotbarSlot != player.getInventory().getSelectedSlot()) { player.networkHandler.sendPacket(new UpdateSelectedSlotC2SPacket(hotbarSlot)); - player.getInventory().selectedSlot = hotbarSlot; + ((me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) player.getInventory()).setSelectedSlot(hotbarSlot); } updateLastUsedSlot(hotbarSlot); return true; @@ -239,22 +239,24 @@ public boolean select(ItemStack itemStack) { // https://github.com/sakura-ryoko/litematica-printer/blob/f8e38a2b31708e61f8a5fad0f2989d6834495da4/src/main/java/me/aleksilassila/litematica/printer/actions/PrepareAction.java#L95 private void addPickBlock(PlayerInventory inv, ItemStack stack) { + me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor invAccessor = + (me.aleksilassila.litematica.printer.v1_21_4.mixin.PlayerInventoryAccessor) inv; int slot = inv.getSlotWithStack(stack); if (slot >= 0 && slot < 9) { - inv.selectedSlot = slot; + invAccessor.setSelectedSlot(slot); } else { if (slot == -1) { - inv.selectedSlot = inv.getSwappableHotbarSlot(); + invAccessor.setSelectedSlot(inv.getSwappableHotbarSlot()); - if (!inv.main.get(inv.selectedSlot).isEmpty()) { + if (!inv.getStack(inv.getSelectedSlot()).isEmpty()) { int empty = inv.getEmptySlot(); if (empty != -1) { - inv.main.set(empty, inv.main.get(inv.selectedSlot)); + inv.setStack(empty, inv.getStack(inv.getSelectedSlot())); } } - inv.main.set(inv.selectedSlot, stack); + inv.setStack(inv.getSelectedSlot(), stack); } else { inv.swapSlotWithHotbar(slot); } @@ -327,10 +329,10 @@ private static int getBestInventorySlotWithItem(ClientPlayerEntity player, ItemS int lowestCount = 0; int lowestSlot = -1; - for (int i = 9; i < inventory.main.size(); ++i) { - if (!(inventory.main.get(i)).isEmpty() && ItemStack.areItemsAndComponentsEqual(itemStack, inventory.main.get(i))) { - if (inventory.main.get(i).getCount() < lowestCount || lowestSlot == -1) { - lowestCount = inventory.main.get(i).getCount(); + for (int i = 9; i < 36; ++i) { + if (!inventory.getStack(i).isEmpty() && ItemStack.areItemsAndComponentsEqual(itemStack, inventory.getStack(i))) { + if (inventory.getStack(i).getCount() < lowestCount || lowestSlot == -1) { + lowestCount = inventory.getStack(i).getCount(); lowestSlot = i; } } @@ -348,7 +350,7 @@ public int getHotbarSlotWithItem(ClientPlayerEntity player, ItemStack itemStack) if (itemStack.isEmpty()) return -1; for (int i = 0; i < 9; ++i) { - if (!inventory.main.get(i).isEmpty() && ItemStack.areItemsEqual(inventory.main.get(i), itemStack)) { + if (!inventory.getStack(i).isEmpty() && ItemStack.areItemsEqual(inventory.getStack(i), itemStack)) { // if (hotbarSlots.get(i).ticksLocked == 0) { return i; // } diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java index 9bc7c3fc1..f14392988 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/MovementHandler.java @@ -207,6 +207,6 @@ static void apply(InputDirections direction) { } static boolean isKeyPressed(KeyBinding keyBinding) { - return InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow().getHandle(), ((MixinAccessorKeyBinding) keyBinding).getBoundKey().getCode()); + return InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow(), ((MixinAccessorKeyBinding) keyBinding).getBoundKey().getCode()); } } diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java index c86c11dab..a99759a3c 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/SchematicConverter.java @@ -15,7 +15,7 @@ public static LitematicaSchematic convertAndReturn(File file, File out) { LitematicaSchematic schematic = LitematicaSchematicAccessor.invokeConstructor(file, FileType.VANILLA_STRUCTURE); schematic.readFromFile(); String fileName = file.getName().replace(".nbt", ""); - schematic.writeToFile(out, fileName, true); + schematic.writeToFile(out.toPath(), fileName, true); LitematicaSchematic newSchem = LitematicaSchematicAccessor.invokeConstructor(new File(out, fileName + ".litematic"), FileType.LITEMATICA_SCHEMATIC); newSchem.readFromFile(); return newSchem; diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java index 84a7a456c..1cac5ed79 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/PresShift.java @@ -11,7 +11,7 @@ public boolean send(MinecraftClient client, ClientPlayerEntity player) { player.input.playerInput = new PlayerInput(player.input.playerInput.forward(), player.input.playerInput.backward(), player.input.playerInput.left(), player.input.playerInput.right(), player.input.playerInput.jump(), true, player.input.playerInput.sprint()); - player.networkHandler.sendPacket(new ClientCommandC2SPacket(player, ClientCommandC2SPacket.Mode.PRESS_SHIFT_KEY)); + player.networkHandler.sendPacket(new ClientCommandC2SPacket(player, ClientCommandC2SPacket.Mode.START_SNEAKING)); return true; } } diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java index 164ec3906..f4ae8b49b 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/actions/ReleaseShiftAction.java @@ -13,7 +13,7 @@ public boolean send(MinecraftClient client, ClientPlayerEntity player) { player.input.playerInput.backward(), player.input.playerInput.left(), player.input.playerInput.right(), player.input.playerInput.jump(), mc.options.sneakKey.isPressed(), player.input.playerInput.sprint()); if (!mc.options.sneakKey.isPressed()) { - player.networkHandler.sendPacket(new ClientCommandC2SPacket(player, ClientCommandC2SPacket.Mode.RELEASE_SHIFT_KEY)); + player.networkHandler.sendPacket(new ClientCommandC2SPacket(player, ClientCommandC2SPacket.Mode.STOP_SNEAKING)); } return true; } diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java index 00d40aed3..3b4441c84 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/guides/Guide.java @@ -36,9 +36,9 @@ protected boolean playerHasRightItem(ClientPlayerEntity player) { public int getSlotWithItem(ClientPlayerEntity player, ItemStack itemStack) { PlayerInventory inventory = player.getInventory(); - for (int i = 0; i < inventory.main.size(); ++i) { - if (itemStack.isEmpty() && inventory.main.get(i).isOf(itemStack.getItem())) return i; - if (!inventory.main.get(i).isEmpty() && ItemStack.areItemsEqual(inventory.main.get(i), itemStack)) { + for (int i = 0; i < 36; ++i) { + if (itemStack.isEmpty() && inventory.getStack(i).isOf(itemStack.getItem())) return i; + if (!inventory.getStack(i).isEmpty() && ItemStack.areItemsEqual(inventory.getStack(i), itemStack)) { return i; } } @@ -48,7 +48,7 @@ public int getSlotWithItem(ClientPlayerEntity player, ItemStack itemStack) { protected int getRequiredItemStackSlot(ClientPlayerEntity player) { if (player.getAbilities().creativeMode) { - return player.getInventory().selectedSlot; + return player.getInventory().getSelectedSlot(); } ItemStack requiredItem = getRequiredItem(player).stream().findFirst().orElse(ItemStack.EMPTY); diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java index f4a673dcc..8d5be35eb 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/ButtonListenerMixin.java @@ -33,7 +33,7 @@ private void onActionPerformedWithButton(ButtonBase button, int mouseButton, Cal if (!warnType.get() || !PrinterConfig.AUTO_CONVERT_SCHEMATIC_TO_LITEMATIC_ON_LOAD.getBooleanValue()) { return; } - LitematicaSchematic newSchem = SchematicConverter.convertAndReturn(schematic.get().getFile(), entry.getDirectory()); + LitematicaSchematic newSchem = SchematicConverter.convertAndReturn(schematic.get().getFile().toFile(), entry.getDirectory()); warnType.set(false); schematic.set(newSchem); InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, 15000, "Auto converted schematic to litematic format"); diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java index 012115f76..efc8811b0 100644 --- a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/MixinMouse.java @@ -33,7 +33,7 @@ private void updateMouseChangeLookDirection(CallbackInfo ci) { double k = this.cursorDeltaX * h; double l = this.cursorDeltaY * h; int m = 1; - if (this.client.options.getInvertYMouse().getValue()) { + if (this.client.options.invertYMouse.getValue()) { m = -1; } float yaw = (float) (freeLook.getCameraYaw() + k * 0.15F); diff --git a/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerInventoryAccessor.java b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerInventoryAccessor.java new file mode 100644 index 000000000..47a91b413 --- /dev/null +++ b/v1_21_4/src/main/java/me/aleksilassila/litematica/printer/v1_21_4/mixin/PlayerInventoryAccessor.java @@ -0,0 +1,11 @@ +package me.aleksilassila.litematica.printer.v1_21_4.mixin; + +import net.minecraft.entity.player.PlayerInventory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(PlayerInventory.class) +public interface PlayerInventoryAccessor { + @Accessor("selectedSlot") + void setSelectedSlot(int slot); +} diff --git a/v1_21_4/src/main/resources/litematica-printer.mixins.json b/v1_21_4/src/main/resources/litematica-printer.mixins.json index 8d1526eec..327b3b52b 100644 --- a/v1_21_4/src/main/resources/litematica-printer.mixins.json +++ b/v1_21_4/src/main/resources/litematica-printer.mixins.json @@ -15,6 +15,7 @@ "InputHandlerMixin", "MixinAccessorClientPlayerEntity", "MixinAccessorKeyBinding", + "PlayerInventoryAccessor", "MixinCamera", "MixinClientPlayerEntity", "MixinMouse",