Initial sanctuary sources
@@ -0,0 +1,34 @@
|
||||
# Gradle
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
classes/
|
||||
|
||||
# Eclipse
|
||||
*.launch
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# VS Code
|
||||
.settings/
|
||||
.vscode/
|
||||
bin/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# macOS
|
||||
*.DS_Store
|
||||
|
||||
# Fabric
|
||||
run/
|
||||
|
||||
# Java
|
||||
.jdk/
|
||||
hs_err_*.log
|
||||
replay_*.log
|
||||
*.hprof
|
||||
*.jfr
|
||||
@@ -0,0 +1,67 @@
|
||||
plugins {
|
||||
id "net.fabricmc.fabric-loom" version "${loom_version}"
|
||||
id "maven-publish"
|
||||
}
|
||||
|
||||
version = project.mod_version
|
||||
group = project.maven_group
|
||||
|
||||
repositories {
|
||||
}
|
||||
|
||||
loom {
|
||||
splitEnvironmentSourceSets()
|
||||
|
||||
mods {
|
||||
"minecrafter" {
|
||||
sourceSet sourceSets.main
|
||||
sourceSet sourceSets.client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
||||
}
|
||||
|
||||
processResources {
|
||||
def version = project.version
|
||||
inputs.property "version", version
|
||||
|
||||
filesMatching("fabric.mod.json") {
|
||||
expand "version": version
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
it.options.release = 25
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_25
|
||||
targetCompatibility = JavaVersion.VERSION_25
|
||||
}
|
||||
|
||||
jar {
|
||||
def projectName = project.name
|
||||
inputs.property "projectName", projectName
|
||||
|
||||
from("../../license.txt") {
|
||||
rename { "${it}_${projectName}" }
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create("mavenJava", MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Done to increase the memory available to Gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.parallel=true
|
||||
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache:
|
||||
# https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Fabric properties
|
||||
# Check current versions on https://fabricmc.net/develop
|
||||
minecraft_version=26.1.2
|
||||
loader_version=0.19.2
|
||||
loom_version=1.16-SNAPSHOT
|
||||
|
||||
# Mod properties
|
||||
mod_version=1.0.0
|
||||
maven_group=fr.koka99cab
|
||||
archives_base_name=minecrafter
|
||||
|
||||
# Dependencies
|
||||
fabric_api_version=0.148.0+26.1.2
|
||||
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# 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/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
if [ -z "$JAVA_HOME" ] && [ -x "$APP_HOME/.jdk/current/bin/java" ] ; then
|
||||
JAVA_HOME=$APP_HOME/.jdk/current
|
||||
export JAVA_HOME
|
||||
fi
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,93 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem 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. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
: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
|
||||
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,12 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = "Fabric"
|
||||
url = "https://maven.fabricmc.net/"
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "minecrafter"
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.koka.heartoverlay;
|
||||
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
|
||||
public final class HeartOverlayClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
HeartOverlayRenderer.register();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.koka.heartoverlay;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.rendertype.RenderType;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.LightCoordsUtil;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class HeartOverlayRenderer {
|
||||
private static final Identifier HEART_CONTAINER_TEXTURE = Identifier.fromNamespaceAndPath("minecraft", "textures/gui/sprites/hud/heart/container.png");
|
||||
private static final Identifier HEART_HALF_TEXTURE = Identifier.fromNamespaceAndPath("minecraft", "textures/gui/sprites/hud/heart/half.png");
|
||||
private static final Identifier HEART_FULL_TEXTURE = Identifier.fromNamespaceAndPath("minecraft", "textures/gui/sprites/hud/heart/full.png");
|
||||
private static final RenderType HEART_CONTAINER_RENDER_TYPE = RenderTypes.entityTranslucentEmissive(HEART_CONTAINER_TEXTURE);
|
||||
private static final RenderType HEART_HALF_RENDER_TYPE = RenderTypes.entityTranslucentEmissive(HEART_HALF_TEXTURE);
|
||||
private static final RenderType HEART_FULL_RENDER_TYPE = RenderTypes.entityTranslucentEmissive(HEART_FULL_TEXTURE);
|
||||
private static final int HEARTS_PER_ROW = 10;
|
||||
private static final int ICON_WIDTH = 9;
|
||||
private static final int ICON_HEIGHT = 9;
|
||||
private static final int HEART_STEP_X = 8;
|
||||
private static final int BASE_LINE_HEIGHT = 10;
|
||||
private static final int MIN_LINE_HEIGHT = 3;
|
||||
private static final float FILL_Z_OFFSET = -0.01F;
|
||||
private static final float LABEL_SCALE = 0.02F;
|
||||
private static final float Y_OFFSET = 0.45F;
|
||||
|
||||
private HeartOverlayRenderer() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
LevelRenderEvents.AFTER_TRANSLUCENT_TERRAIN.register(HeartOverlayRenderer::render);
|
||||
}
|
||||
|
||||
private static void render(LevelRenderContext context) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (client.options.hideGui || client.level == null || client.player == null || client.hitResult == null || !client.player.isCrouching()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LivingEntity target = getTargetedMob(client.hitResult);
|
||||
if (target == null || target.isDeadOrDying()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PoseStack matrices = context.poseStack();
|
||||
if (matrices == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Camera camera = client.gameRenderer.getMainCamera();
|
||||
float tickDelta = client.getDeltaTracker().getGameTimeDeltaPartialTick(false);
|
||||
Vec3 entityPos = target.getPosition(tickDelta);
|
||||
Vec3 cameraPos = camera.position();
|
||||
|
||||
matrices.pushPose();
|
||||
matrices.translate(entityPos.x - cameraPos.x, entityPos.y - cameraPos.y + target.getBbHeight() + Y_OFFSET, entityPos.z - cameraPos.z);
|
||||
matrices.mulPose(camera.rotation());
|
||||
matrices.scale(-LABEL_SCALE, -LABEL_SCALE, LABEL_SCALE);
|
||||
|
||||
renderHearts(matrices, context.bufferSource(), target);
|
||||
|
||||
matrices.popPose();
|
||||
}
|
||||
|
||||
private static LivingEntity getTargetedMob(HitResult hitResult) {
|
||||
if (!(hitResult instanceof EntityHitResult entityHitResult)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(entityHitResult.getEntity() instanceof LivingEntity livingEntity)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (livingEntity instanceof Player) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return livingEntity;
|
||||
}
|
||||
|
||||
private static void renderHearts(PoseStack matrices, MultiBufferSource consumers, LivingEntity entity) {
|
||||
float maxHealth = Math.max(entity.getMaxHealth(), 1.0F);
|
||||
float currentHealth = Mth.clamp(entity.getHealth(), 0.0F, maxHealth);
|
||||
int totalSlots = Math.max(1, Mth.ceil(maxHealth / 2.0F));
|
||||
int rows = Mth.ceil(totalSlots / (float) HEARTS_PER_ROW);
|
||||
int lineHeight = Math.max(BASE_LINE_HEIGHT - (rows - 2), MIN_LINE_HEIGHT);
|
||||
PoseStack.Pose pose = matrices.last();
|
||||
|
||||
for (int index = totalSlots - 1; index >= 0; index--) {
|
||||
float left = heartLeft(index, totalSlots);
|
||||
float top = heartTop(index, lineHeight);
|
||||
drawHeart(consumers.getBuffer(HEART_CONTAINER_RENDER_TYPE), pose, left, top, 0.0F);
|
||||
}
|
||||
|
||||
for (int index = totalSlots - 1; index >= 0; index--) {
|
||||
HeartState state = stateForSlot(index, currentHealth);
|
||||
if (state != HeartState.EMPTY) {
|
||||
float left = heartLeft(index, totalSlots);
|
||||
float top = heartTop(index, lineHeight);
|
||||
drawHeart(consumers.getBuffer(renderTypeFor(state)), pose, left, top, FILL_Z_OFFSET, state == HeartState.HALF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float heartLeft(int index, int totalSlots) {
|
||||
int row = index / HEARTS_PER_ROW;
|
||||
int column = index % HEARTS_PER_ROW;
|
||||
int heartsInRow = Math.min(HEARTS_PER_ROW, totalSlots - row * HEARTS_PER_ROW);
|
||||
int mirroredColumn = heartsInRow - 1 - column;
|
||||
float rowWidth = ICON_WIDTH + (heartsInRow - 1) * HEART_STEP_X;
|
||||
return -rowWidth / 2.0F + mirroredColumn * HEART_STEP_X;
|
||||
}
|
||||
|
||||
private static float heartTop(int index, int lineHeight) {
|
||||
int row = index / HEARTS_PER_ROW;
|
||||
return -row * lineHeight;
|
||||
}
|
||||
|
||||
private static HeartState stateForSlot(int slotIndex, float currentHealth) {
|
||||
float slotStart = slotIndex * 2.0F;
|
||||
float remaining = currentHealth - slotStart;
|
||||
if (remaining >= 2.0F) {
|
||||
return HeartState.FULL;
|
||||
}
|
||||
if (remaining >= 1.0F) {
|
||||
return HeartState.HALF;
|
||||
}
|
||||
return HeartState.EMPTY;
|
||||
}
|
||||
|
||||
private static RenderType renderTypeFor(HeartState state) {
|
||||
return switch (state) {
|
||||
case FULL -> HEART_FULL_RENDER_TYPE;
|
||||
case HALF -> HEART_HALF_RENDER_TYPE;
|
||||
case EMPTY -> HEART_CONTAINER_RENDER_TYPE;
|
||||
};
|
||||
}
|
||||
|
||||
private static void drawHeart(VertexConsumer consumer, PoseStack.Pose pose, float left, float top, float z) {
|
||||
drawHeart(consumer, pose, left, top, z, false);
|
||||
}
|
||||
|
||||
private static void drawHeart(VertexConsumer consumer, PoseStack.Pose pose, float left, float top, float z, boolean flipU) {
|
||||
float right = left + ICON_WIDTH;
|
||||
float bottom = top + ICON_HEIGHT;
|
||||
float minU = flipU ? 1.0F : 0.0F;
|
||||
float maxU = flipU ? 0.0F : 1.0F;
|
||||
|
||||
consumer.addVertex(pose, left, bottom, z).setColor(255, 255, 255, 255).setUv(minU, 1.0F).setOverlay(OverlayTexture.NO_OVERLAY).setLight(LightCoordsUtil.FULL_BRIGHT).setNormal(pose, 0.0F, 0.0F, -1.0F);
|
||||
consumer.addVertex(pose, right, bottom, z).setColor(255, 255, 255, 255).setUv(maxU, 1.0F).setOverlay(OverlayTexture.NO_OVERLAY).setLight(LightCoordsUtil.FULL_BRIGHT).setNormal(pose, 0.0F, 0.0F, -1.0F);
|
||||
consumer.addVertex(pose, right, top, z).setColor(255, 255, 255, 255).setUv(maxU, 0.0F).setOverlay(OverlayTexture.NO_OVERLAY).setLight(LightCoordsUtil.FULL_BRIGHT).setNormal(pose, 0.0F, 0.0F, -1.0F);
|
||||
consumer.addVertex(pose, left, top, z).setColor(255, 255, 255, 255).setUv(minU, 0.0F).setOverlay(OverlayTexture.NO_OVERLAY).setLight(LightCoordsUtil.FULL_BRIGHT).setNormal(pose, 0.0F, 0.0F, -1.0F);
|
||||
}
|
||||
|
||||
private enum HeartState {
|
||||
FULL,
|
||||
HALF,
|
||||
EMPTY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package dev.koka.previewer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.QuadInstance;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.LightCoordsUtil;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class PreviewerClient implements ClientModInitializer {
|
||||
private static final int GHOST_ALPHA_MIN = 128;
|
||||
private static final int GHOST_ALPHA_MAX = 192;
|
||||
private static final long GHOST_PULSE_PERIOD_MS = 1600L;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
LevelRenderEvents.COLLECT_SUBMITS.register(PreviewerClient::renderPreview);
|
||||
}
|
||||
|
||||
private static void renderPreview(LevelRenderContext context) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
|
||||
if (client.player == null || client.level == null || client.player.isSpectator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.player.isCrouching() || !(client.hitResult instanceof BlockHitResult hitResult) || hitResult.getType() != HitResult.Type.BLOCK) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlacementPreview preview = findPreview(client, hitResult);
|
||||
|
||||
if (preview == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitBlockModel(client, context, preview);
|
||||
}
|
||||
|
||||
private static PlacementPreview findPreview(Minecraft client, BlockHitResult hitResult) {
|
||||
PlacementPreview mainHandPreview = findPreview(client, hitResult, InteractionHand.MAIN_HAND);
|
||||
|
||||
if (mainHandPreview != null) {
|
||||
return mainHandPreview;
|
||||
}
|
||||
|
||||
return findPreview(client, hitResult, InteractionHand.OFF_HAND);
|
||||
}
|
||||
|
||||
private static PlacementPreview findPreview(Minecraft client, BlockHitResult hitResult, InteractionHand hand) {
|
||||
ItemStack stack = client.player.getItemInHand(hand);
|
||||
|
||||
if (!(stack.getItem() instanceof BlockItem blockItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockPlaceContext initialContext = new BlockPlaceContext(client.player, hand, stack, hitResult);
|
||||
BlockPlaceContext placementContext = blockItem.updatePlacementContext(initialContext);
|
||||
|
||||
if (placementContext == null || !placementContext.canPlace()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockPos pos = placementContext.getClickedPos().immutable();
|
||||
BlockState state = blockItem.getBlock().getStateForPlacement(placementContext);
|
||||
|
||||
if (state == null || state.isAir() || !state.canSurvive(client.level, pos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PlacementPreview(pos, state);
|
||||
}
|
||||
|
||||
private static void submitBlockModel(Minecraft client, LevelRenderContext context, PlacementPreview preview) {
|
||||
if (preview.state().getRenderShape() != RenderShape.MODEL || context.poseStack() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<BlockStateModelPart> parts = modelParts(client, preview);
|
||||
if (parts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Camera camera = client.gameRenderer.getMainCamera();
|
||||
Vec3 cameraPos = camera.position();
|
||||
BlockPos pos = preview.pos();
|
||||
PoseStack matrices = context.poseStack();
|
||||
|
||||
matrices.pushPose();
|
||||
matrices.translate(pos.getX() - cameraPos.x, pos.getY() - cameraPos.y, pos.getZ() - cameraPos.z);
|
||||
context.submitNodeCollector().submitCustomGeometry(
|
||||
matrices,
|
||||
RenderTypes.translucentMovingBlock(),
|
||||
(pose, consumer) -> renderModelParts(pose, consumer, parts, getPulsingAlpha())
|
||||
);
|
||||
matrices.popPose();
|
||||
}
|
||||
|
||||
private static List<BlockStateModelPart> modelParts(Minecraft client, PlacementPreview preview) {
|
||||
BlockStateModel model = client.getModelManager().getBlockStateModelSet().get(preview.state());
|
||||
List<BlockStateModelPart> parts = new ArrayList<>();
|
||||
model.collectParts(RandomSource.create(preview.state().getSeed(preview.pos())), parts);
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static void renderModelParts(PoseStack.Pose pose, VertexConsumer consumer, List<BlockStateModelPart> parts, int alpha) {
|
||||
QuadInstance quad = new QuadInstance();
|
||||
quad.setColor((alpha << 24) | 0xFFFFFF);
|
||||
quad.setLightCoords(LightCoordsUtil.FULL_BRIGHT);
|
||||
quad.setOverlayCoords(OverlayTexture.NO_OVERLAY);
|
||||
|
||||
for (BlockStateModelPart part : parts) {
|
||||
renderQuads(pose, consumer, part.getQuads(null), quad);
|
||||
|
||||
for (Direction direction : Direction.values()) {
|
||||
renderQuads(pose, consumer, part.getQuads(direction), quad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderQuads(PoseStack.Pose pose, VertexConsumer consumer, List<BakedQuad> quads, QuadInstance quad) {
|
||||
for (BakedQuad bakedQuad : quads) {
|
||||
consumer.putBakedQuad(pose, bakedQuad, quad);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getPulsingAlpha() {
|
||||
double phase = (System.currentTimeMillis() % GHOST_PULSE_PERIOD_MS) / (double) GHOST_PULSE_PERIOD_MS;
|
||||
double wave = (Math.sin(phase * Math.PI * 2.0D - Math.PI / 2.0D) + 1.0D) * 0.5D;
|
||||
|
||||
return GHOST_ALPHA_MIN + (int) Math.round(wave * (GHOST_ALPHA_MAX - GHOST_ALPHA_MIN));
|
||||
}
|
||||
|
||||
private record PlacementPreview(BlockPos pos, BlockState state) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.koka.builder.client;
|
||||
|
||||
import fr.koka.builder.BuilderMod;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class BuilderClientKeys {
|
||||
public static final KeyMapping.Category BUILDER_CATEGORY =
|
||||
KeyMapping.Category.register(Identifier.fromNamespaceAndPath(BuilderMod.MOD_ID, "builder"));
|
||||
|
||||
private BuilderClientKeys() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package fr.koka.builder.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.builder.BuildSelection;
|
||||
import fr.koka.builder.BuilderConfig;
|
||||
import fr.koka.builder.BuilderControlPayload;
|
||||
import fr.koka.builder.BuilderMod;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class BuilderClientMod implements ClientModInitializer {
|
||||
private static final int SWING_INTERVAL_TICKS = 4;
|
||||
private static final Vec3 ZERO_OFFSET = new Vec3(0.0D, 0.0D, 0.0D);
|
||||
private static KeyMapping buildModeKey;
|
||||
private static BlockPos activeOrigin;
|
||||
private static Direction activeFace;
|
||||
private static Vec3 activeHitOffset = ZERO_OFFSET;
|
||||
private static BuildTarget lockedTarget;
|
||||
private static int currentBuildPower = BuilderConfig.DEFAULT_BUILD_POWER;
|
||||
private static int activeBuildPower = BuilderConfig.DEFAULT_BUILD_POWER;
|
||||
private static List<BlockPos> previewSelection = List.of();
|
||||
private static int buildAnimationTicks;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
buildModeKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.builder.build_mode",
|
||||
InputConstants.Type.KEYSYM,
|
||||
GLFW.GLFW_KEY_B,
|
||||
BuilderClientKeys.BUILDER_CATEGORY
|
||||
));
|
||||
|
||||
HudElementRegistry.addLast(BuilderMod.id("selection_count"), BuilderClientMod::renderSelectionCount);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(BuilderClientMod::onEndClientTick);
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> resetClientState());
|
||||
UseBlockCallback.EVENT.register((player, world, hand, hitResult) ->
|
||||
world.isClientSide() ? onClientUseBlock(Minecraft.getInstance(), hand, hitResult) : InteractionResult.PASS
|
||||
);
|
||||
}
|
||||
|
||||
private static void onEndClientTick(Minecraft client) {
|
||||
if (client.level == null || client.player == null) {
|
||||
resetClientState();
|
||||
return;
|
||||
}
|
||||
|
||||
BuildTarget liveTarget = resolveCurrentTarget(client);
|
||||
updateLockedTarget(client, liveTarget);
|
||||
BuildTarget target = activeTarget(liveTarget);
|
||||
updatePreviewSelection(client, target);
|
||||
BuilderPreviewRenderer.collect(client, previewSelection);
|
||||
|
||||
if (activeOrigin != null && (target == null || previewSelection.isEmpty() || !shouldContinueBuilding(client, target))) {
|
||||
stopBuilding(client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target == null || previewSelection.isEmpty() || !client.options.keyUse.isDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean targetChanged = activeOrigin == null
|
||||
|| !target.origin().equals(activeOrigin)
|
||||
|| target.face() != activeFace
|
||||
|| currentBuildPower != activeBuildPower;
|
||||
|
||||
if (targetChanged) {
|
||||
sendBuildPacket(client, true, target);
|
||||
activeOrigin = target.origin();
|
||||
activeFace = target.face();
|
||||
activeHitOffset = target.hitOffset();
|
||||
activeBuildPower = currentBuildPower;
|
||||
buildAnimationTicks = 0;
|
||||
client.player.swing(InteractionHand.MAIN_HAND);
|
||||
return;
|
||||
}
|
||||
|
||||
buildAnimationTicks++;
|
||||
if (buildAnimationTicks >= SWING_INTERVAL_TICKS) {
|
||||
client.player.swing(InteractionHand.MAIN_HAND);
|
||||
buildAnimationTicks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updatePreviewSelection(Minecraft client) {
|
||||
updatePreviewSelection(client, activeTarget(resolveCurrentTarget(client)));
|
||||
}
|
||||
|
||||
private static void updatePreviewSelection(Minecraft client, BuildTarget target) {
|
||||
previewSelection = selectionForTarget(client, target);
|
||||
}
|
||||
|
||||
private static void updateLockedTarget(Minecraft client, BuildTarget liveTarget) {
|
||||
if (!isBuildModeActive(client) || !client.options.keyUse.isDown()) {
|
||||
lockedTarget = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockedTarget == null && liveTarget != null) {
|
||||
lockedTarget = liveTarget;
|
||||
}
|
||||
}
|
||||
|
||||
private static BuildTarget activeTarget(BuildTarget liveTarget) {
|
||||
return lockedTarget != null ? lockedTarget : liveTarget;
|
||||
}
|
||||
|
||||
private static List<BlockPos> selectionForTarget(Minecraft client, BuildTarget target) {
|
||||
if (target == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return BuildSelection.collectPreview(client.player, client.level, target.origin(), target.face(), target.hitOffset(), currentBuildPower);
|
||||
}
|
||||
|
||||
private static boolean shouldContinueBuilding(Minecraft client, BuildTarget target) {
|
||||
return buildModeKey != null
|
||||
&& buildModeKey.isDown()
|
||||
&& client.screen == null
|
||||
&& client.options.keyUse.isDown()
|
||||
&& activeOrigin != null
|
||||
&& activeOrigin.equals(target.origin())
|
||||
&& activeFace == target.face()
|
||||
&& BuildSelection.canUseBuilderItem(client.player);
|
||||
}
|
||||
|
||||
private static void stopBuilding(Minecraft client) {
|
||||
if (activeOrigin == null || activeFace == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.getConnection() != null) {
|
||||
ClientPlayNetworking.send(new BuilderControlPayload(false, activeOrigin, activeFace, activeHitOffset, currentBuildPower));
|
||||
}
|
||||
|
||||
activeOrigin = null;
|
||||
activeFace = null;
|
||||
activeHitOffset = ZERO_OFFSET;
|
||||
activeBuildPower = currentBuildPower;
|
||||
buildAnimationTicks = 0;
|
||||
}
|
||||
|
||||
public static boolean onMouseScroll(double horizontalAmount, double verticalAmount) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
|
||||
if (client.player == null || client.screen != null || buildModeKey == null || !buildModeKey.isDown()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int direction = scrollDirection(horizontalAmount, verticalAmount);
|
||||
|
||||
if (direction == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int nextPower = BuilderConfig.clampBuildPower(currentBuildPower + direction * BuilderConfig.BUILD_POWER_SCROLL_STEP);
|
||||
|
||||
if (nextPower == currentBuildPower) {
|
||||
showBuildPowerMessage(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
currentBuildPower = nextPower;
|
||||
showBuildPowerMessage(client);
|
||||
updatePreviewSelection(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldReplaceCrosshair() {
|
||||
return buildModeKey != null && buildModeKey.isDown() && !previewSelection.isEmpty();
|
||||
}
|
||||
|
||||
public static Component crosshairReplacementText() {
|
||||
if (!shouldReplaceCrosshair()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Component.literal(Integer.toString(previewSelection.size()));
|
||||
}
|
||||
|
||||
public static boolean shouldCancelVanillaUse(Minecraft client) {
|
||||
if (client.player == null || client.level == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isBuildModeActive(client) || !BuildSelection.canUseBuilderItem(client.player)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BuildTarget target = activeTarget(resolveCurrentTarget(client));
|
||||
return target != null && (lockedTarget != null || !selectionForTarget(client, target).isEmpty());
|
||||
}
|
||||
|
||||
private static void sendBuildPacket(Minecraft client, boolean active, BuildTarget target) {
|
||||
if (client.getConnection() != null) {
|
||||
ClientPlayNetworking.send(new BuilderControlPayload(active, target.origin(), target.face(), target.hitOffset(), currentBuildPower));
|
||||
}
|
||||
}
|
||||
|
||||
private static int scrollDirection(double horizontalAmount, double verticalAmount) {
|
||||
if (verticalAmount > 0.0D) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verticalAmount < 0.0D) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (horizontalAmount > 0.0D) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (horizontalAmount < 0.0D) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void showBuildPowerMessage(Minecraft client) {
|
||||
client.player.sendOverlayMessage(
|
||||
Component.translatable(
|
||||
"message.builder.build_power",
|
||||
currentBuildPower,
|
||||
BuilderConfig.buildDiameter(currentBuildPower)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static void renderSelectionCount(GuiGraphicsExtractor graphics, net.minecraft.client.DeltaTracker deltaTracker) {
|
||||
Component selectionText = crosshairReplacementText();
|
||||
|
||||
if (selectionText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Font font = Minecraft.getInstance().font;
|
||||
int x = (graphics.guiWidth() - font.width(selectionText)) / 2;
|
||||
int y = (graphics.guiHeight() - font.lineHeight) / 2;
|
||||
graphics.text(font, selectionText, x, y, 0xFFFFFFFF, true);
|
||||
}
|
||||
|
||||
private static InteractionResult onClientUseBlock(Minecraft client, InteractionHand hand, BlockHitResult hitResult) {
|
||||
if (client.player == null || client.level == null || hand != InteractionHand.MAIN_HAND) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
if (!isBuildModeActive(client) || !BuildSelection.canUseBuilderItem(client.player)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
List<BlockPos> selection = selectionForTarget(client, buildTarget(hitResult));
|
||||
return selection.isEmpty() ? InteractionResult.PASS : InteractionResult.FAIL;
|
||||
}
|
||||
|
||||
private static BuildTarget resolveCurrentTarget(Minecraft client) {
|
||||
if (!isBuildModeActive(client)
|
||||
|| client.player == null
|
||||
|| client.level == null
|
||||
|| !(client.hitResult instanceof BlockHitResult blockHitResult)
|
||||
|| blockHitResult.getType() == HitResult.Type.MISS
|
||||
|| !BuildSelection.canUseBuilderItem(client.player)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildTarget(blockHitResult);
|
||||
}
|
||||
|
||||
private static BuildTarget buildTarget(BlockHitResult blockHitResult) {
|
||||
return new BuildTarget(
|
||||
blockHitResult.getBlockPos().immutable(),
|
||||
blockHitResult.getDirection(),
|
||||
BuildSelection.localHitOffset(blockHitResult)
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean isBuildModeActive(Minecraft client) {
|
||||
return buildModeKey != null && buildModeKey.isDown() && client.screen == null;
|
||||
}
|
||||
|
||||
private static void resetClientState() {
|
||||
activeOrigin = null;
|
||||
activeFace = null;
|
||||
activeHitOffset = ZERO_OFFSET;
|
||||
lockedTarget = null;
|
||||
activeBuildPower = currentBuildPower;
|
||||
previewSelection = List.of();
|
||||
buildAnimationTicks = 0;
|
||||
}
|
||||
|
||||
private record BuildTarget(BlockPos origin, Direction face, Vec3 hitOffset) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package fr.koka.builder.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gizmos.Gizmos;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class BuilderPreviewRenderer {
|
||||
private static final int OUTLINE_COLOR = 0x4B000000;
|
||||
private static final double FACE_THICKNESS = 0.025D;
|
||||
private static final double OUTLINE_OFFSET = 0.002D;
|
||||
private static final float OUTLINE_WIDTH = 1.08F;
|
||||
|
||||
private BuilderPreviewRenderer() {
|
||||
}
|
||||
|
||||
public static void collect(Minecraft client, List<BlockPos> selection) {
|
||||
if (selection.isEmpty() || client.level == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewMesh previewMesh = buildPreviewMesh(selection);
|
||||
|
||||
try (Gizmos.TemporaryCollection ignored = client.collectPerTickGizmos()) {
|
||||
for (LineSegment segment : previewMesh.outlineSegments()) {
|
||||
Gizmos.line(segment.start(), segment.end(), OUTLINE_COLOR, OUTLINE_WIDTH)
|
||||
.setAlwaysOnTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PreviewMesh buildPreviewMesh(List<BlockPos> selection) {
|
||||
Set<BlockPos> blocks = new HashSet<>();
|
||||
|
||||
for (BlockPos pos : selection) {
|
||||
blocks.add(pos.immutable());
|
||||
}
|
||||
|
||||
Map<Direction, Map<Integer, Set<FaceCell>>> facesByDirection = new EnumMap<>(Direction.class);
|
||||
|
||||
for (Direction direction : Direction.values()) {
|
||||
facesByDirection.put(direction, new java.util.HashMap<>());
|
||||
}
|
||||
|
||||
for (BlockPos pos : blocks) {
|
||||
for (Direction direction : Direction.values()) {
|
||||
if (blocks.contains(pos.relative(direction))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int plane = planeOf(pos, direction);
|
||||
facesByDirection.get(direction)
|
||||
.computeIfAbsent(plane, ignored -> new HashSet<>())
|
||||
.add(faceCellOf(pos, direction));
|
||||
}
|
||||
}
|
||||
|
||||
List<AABB> faceBoxes = new ArrayList<>();
|
||||
List<LineSegment> outlineSegments = new ArrayList<>();
|
||||
|
||||
for (Direction direction : Direction.values()) {
|
||||
for (Map.Entry<Integer, Set<FaceCell>> entry : facesByDirection.get(direction).entrySet()) {
|
||||
faceBoxes.addAll(mergeFacePlane(direction, entry.getKey(), entry.getValue()));
|
||||
outlineSegments.addAll(outlineSegmentsForPlane(direction, entry.getKey(), entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
return new PreviewMesh(faceBoxes, outlineSegments);
|
||||
}
|
||||
|
||||
private static List<AABB> mergeFacePlane(Direction direction, int plane, Set<FaceCell> faceCells) {
|
||||
Set<FaceCell> remaining = new HashSet<>(faceCells);
|
||||
List<AABB> boxes = new ArrayList<>();
|
||||
|
||||
while (!remaining.isEmpty()) {
|
||||
FaceCell start = pickLowest(remaining);
|
||||
int maxU = extendU(remaining, start);
|
||||
int maxV = extendV(remaining, start, maxU);
|
||||
|
||||
for (int u = start.u(); u <= maxU; u++) {
|
||||
for (int v = start.v(); v <= maxV; v++) {
|
||||
remaining.remove(new FaceCell(u, v));
|
||||
}
|
||||
}
|
||||
|
||||
boxes.add(faceBox(direction, plane, start.u(), start.v(), maxU + 1, maxV + 1));
|
||||
}
|
||||
|
||||
return boxes;
|
||||
}
|
||||
|
||||
private static List<LineSegment> outlineSegmentsForPlane(Direction direction, int plane, Set<FaceCell> faceCells) {
|
||||
Set<FaceEdge> edges = new HashSet<>();
|
||||
|
||||
for (FaceCell cell : faceCells) {
|
||||
toggleEdge(edges, FaceEdge.of(cell.u(), cell.v(), cell.u() + 1, cell.v()));
|
||||
toggleEdge(edges, FaceEdge.of(cell.u() + 1, cell.v(), cell.u() + 1, cell.v() + 1));
|
||||
toggleEdge(edges, FaceEdge.of(cell.u() + 1, cell.v() + 1, cell.u(), cell.v() + 1));
|
||||
toggleEdge(edges, FaceEdge.of(cell.u(), cell.v() + 1, cell.u(), cell.v()));
|
||||
}
|
||||
|
||||
List<LineSegment> segments = new ArrayList<>();
|
||||
|
||||
for (FaceEdge edge : edges) {
|
||||
segments.add(new LineSegment(
|
||||
facePoint(direction, plane, edge.u1(), edge.v1()),
|
||||
facePoint(direction, plane, edge.u2(), edge.v2())
|
||||
));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static void toggleEdge(Set<FaceEdge> edges, FaceEdge edge) {
|
||||
if (!edges.add(edge)) {
|
||||
edges.remove(edge);
|
||||
}
|
||||
}
|
||||
|
||||
private static FaceCell pickLowest(Set<FaceCell> cells) {
|
||||
FaceCell best = null;
|
||||
|
||||
for (FaceCell cell : cells) {
|
||||
if (best == null || cell.v() < best.v() || (cell.v() == best.v() && cell.u() < best.u())) {
|
||||
best = cell;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int extendU(Set<FaceCell> cells, FaceCell start) {
|
||||
int maxU = start.u();
|
||||
|
||||
while (cells.contains(new FaceCell(maxU + 1, start.v()))) {
|
||||
maxU++;
|
||||
}
|
||||
|
||||
return maxU;
|
||||
}
|
||||
|
||||
private static int extendV(Set<FaceCell> cells, FaceCell start, int maxU) {
|
||||
int maxV = start.v();
|
||||
|
||||
while (canExtendV(cells, start, maxU, maxV + 1)) {
|
||||
maxV++;
|
||||
}
|
||||
|
||||
return maxV;
|
||||
}
|
||||
|
||||
private static boolean canExtendV(Set<FaceCell> cells, FaceCell start, int maxU, int nextV) {
|
||||
for (int u = start.u(); u <= maxU; u++) {
|
||||
if (!cells.contains(new FaceCell(u, nextV))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int planeOf(BlockPos pos, Direction direction) {
|
||||
return switch (direction) {
|
||||
case WEST -> pos.getX();
|
||||
case EAST -> pos.getX() + 1;
|
||||
case DOWN -> pos.getY();
|
||||
case UP -> pos.getY() + 1;
|
||||
case NORTH -> pos.getZ();
|
||||
case SOUTH -> pos.getZ() + 1;
|
||||
};
|
||||
}
|
||||
|
||||
private static FaceCell faceCellOf(BlockPos pos, Direction direction) {
|
||||
return switch (direction) {
|
||||
case WEST, EAST -> new FaceCell(pos.getY(), pos.getZ());
|
||||
case DOWN, UP -> new FaceCell(pos.getX(), pos.getZ());
|
||||
case NORTH, SOUTH -> new FaceCell(pos.getX(), pos.getY());
|
||||
};
|
||||
}
|
||||
|
||||
private static AABB faceBox(Direction direction, int plane, int minU, int minV, int maxUExclusive, int maxVExclusive) {
|
||||
return switch (direction) {
|
||||
case WEST -> new AABB(plane, minU, minV, plane + FACE_THICKNESS, maxUExclusive, maxVExclusive);
|
||||
case EAST -> new AABB(plane - FACE_THICKNESS, minU, minV, plane, maxUExclusive, maxVExclusive);
|
||||
case DOWN -> new AABB(minU, plane, minV, maxUExclusive, plane + FACE_THICKNESS, maxVExclusive);
|
||||
case UP -> new AABB(minU, plane - FACE_THICKNESS, minV, maxUExclusive, plane, maxVExclusive);
|
||||
case NORTH -> new AABB(minU, minV, plane, maxUExclusive, maxVExclusive, plane + FACE_THICKNESS);
|
||||
case SOUTH -> new AABB(minU, minV, plane - FACE_THICKNESS, maxUExclusive, maxVExclusive, plane);
|
||||
};
|
||||
}
|
||||
|
||||
private static Vec3 facePoint(Direction direction, int plane, int u, int v) {
|
||||
double offsetX = direction.getStepX() * OUTLINE_OFFSET;
|
||||
double offsetY = direction.getStepY() * OUTLINE_OFFSET;
|
||||
double offsetZ = direction.getStepZ() * OUTLINE_OFFSET;
|
||||
|
||||
return switch (direction) {
|
||||
case WEST, EAST -> new Vec3(plane + offsetX, u + offsetY, v + offsetZ);
|
||||
case DOWN, UP -> new Vec3(u + offsetX, plane + offsetY, v + offsetZ);
|
||||
case NORTH, SOUTH -> new Vec3(u + offsetX, v + offsetY, plane + offsetZ);
|
||||
};
|
||||
}
|
||||
|
||||
private record FaceCell(int u, int v) {
|
||||
}
|
||||
|
||||
private record FaceEdge(int u1, int v1, int u2, int v2) {
|
||||
private static FaceEdge of(int u1, int v1, int u2, int v2) {
|
||||
if (u1 < u2 || (u1 == u2 && v1 <= v2)) {
|
||||
return new FaceEdge(u1, v1, u2, v2);
|
||||
}
|
||||
|
||||
return new FaceEdge(u2, v2, u1, v1);
|
||||
}
|
||||
}
|
||||
|
||||
private record LineSegment(Vec3 start, Vec3 end) {
|
||||
}
|
||||
|
||||
private record PreviewMesh(List<AABB> faceBoxes, List<LineSegment> outlineSegments) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.builder.client.mixin;
|
||||
|
||||
import fr.koka.builder.client.BuilderClientMod;
|
||||
import net.minecraft.client.Minecraft;
|
||||
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(Minecraft.class)
|
||||
public abstract class MinecraftMixin {
|
||||
@Shadow private int rightClickDelay;
|
||||
|
||||
@Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true)
|
||||
private void builder$cancelVanillaUseWhileBuilding(CallbackInfo ci) {
|
||||
Minecraft client = (Minecraft) (Object) this;
|
||||
|
||||
if (!BuilderClientMod.shouldCancelVanillaUse(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rightClickDelay = 4;
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.builder.client.mixin;
|
||||
|
||||
import fr.koka.builder.client.BuilderClientMod;
|
||||
import net.minecraft.client.MouseHandler;
|
||||
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(MouseHandler.class)
|
||||
public abstract class MouseHandlerMixin {
|
||||
@Inject(method = "onScroll", at = @At("HEAD"), cancellable = true)
|
||||
private void builder$onScroll(long windowPointer, double horizontalAmount, double verticalAmount, CallbackInfo ci) {
|
||||
if (BuilderClientMod.onMouseScroll(horizontalAmount, verticalAmount)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package fr.koka.miner.client;
|
||||
|
||||
import fr.koka.miner.MinerMod;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class MinerClientKeys {
|
||||
public static final KeyMapping.Category MINER_CATEGORY =
|
||||
KeyMapping.Category.register(Identifier.fromNamespaceAndPath(MinerMod.MOD_ID, "miner"));
|
||||
|
||||
private MinerClientKeys() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package fr.koka.miner.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.event.client.player.ClientPreAttackCallback;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||
import fr.koka.miner.MinerMiningPayload;
|
||||
import fr.koka.miner.MinerConfig;
|
||||
import fr.koka.miner.MinerMod;
|
||||
import fr.koka.miner.VeinSelection;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class MinerClientMod implements ClientModInitializer {
|
||||
private static final int SWING_INTERVAL_TICKS = 4;
|
||||
private static final float HIT_SOUND_VOLUME = 0.7F;
|
||||
private static final float HIT_SOUND_PITCH_MULTIPLIER = 0.6F;
|
||||
private static KeyMapping veinModeKey;
|
||||
private static BlockPos activeMiningOrigin;
|
||||
private static int currentVeinPower = MinerConfig.DEFAULT_VEIN_POWER;
|
||||
private static int activeMiningPower = MinerConfig.DEFAULT_VEIN_POWER;
|
||||
private static List<BlockPos> previewSelection = List.of();
|
||||
private static int miningAnimationTicks;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
veinModeKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.miner.vein_mode",
|
||||
InputConstants.Type.KEYSYM,
|
||||
GLFW.GLFW_KEY_M,
|
||||
MinerClientKeys.MINER_CATEGORY
|
||||
));
|
||||
|
||||
HudElementRegistry.addLast(MinerMod.id("selection_count"), MinerClientMod::renderSelectionCount);
|
||||
ClientPreAttackCallback.EVENT.register(MinerClientMod::onClientPreAttack);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(MinerClientMod::onEndClientTick);
|
||||
}
|
||||
|
||||
private static void onEndClientTick(Minecraft client) {
|
||||
if (client.level == null || client.player == null) {
|
||||
stopMining(client);
|
||||
previewSelection = List.of();
|
||||
return;
|
||||
}
|
||||
|
||||
updatePreviewSelection(client);
|
||||
MinerPreviewRenderer.collect(client, previewSelection);
|
||||
|
||||
if (activeMiningOrigin != null && previewSelection.isEmpty()) {
|
||||
stopMining(client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeMiningOrigin != null && !shouldContinueMining(client)) {
|
||||
stopMining(client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeMiningOrigin != null) {
|
||||
miningAnimationTicks++;
|
||||
|
||||
if (miningAnimationTicks >= SWING_INTERVAL_TICKS) {
|
||||
playMiningHitSound(client, activeMiningOrigin);
|
||||
client.player.swing(InteractionHand.MAIN_HAND);
|
||||
miningAnimationTicks = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean onClientPreAttack(Minecraft client, LocalPlayer player, int clickCount) {
|
||||
if (!veinModeKey.isDown() || client.level == null || !(client.hitResult instanceof BlockHitResult blockHitResult) || blockHitResult.getType() == HitResult.Type.MISS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlockPos origin = blockHitResult.getBlockPos();
|
||||
BlockState originState = client.level.getBlockState(origin);
|
||||
List<BlockPos> selection = VeinSelection.collectPreview(client.level, origin, originState, currentVeinPower);
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
previewSelection = selection;
|
||||
|
||||
boolean originChanged = !origin.equals(activeMiningOrigin);
|
||||
boolean powerChanged = currentVeinPower != activeMiningPower;
|
||||
|
||||
if (originChanged) {
|
||||
stopMining(client);
|
||||
sendMiningPacket(client, true, origin);
|
||||
activeMiningOrigin = origin;
|
||||
activeMiningPower = currentVeinPower;
|
||||
} else if (powerChanged) {
|
||||
sendMiningPacket(client, true, origin);
|
||||
activeMiningPower = currentVeinPower;
|
||||
}
|
||||
|
||||
if (clickCount > 0 || originChanged || powerChanged) {
|
||||
playMiningHitSound(client, origin);
|
||||
player.swing(InteractionHand.MAIN_HAND);
|
||||
miningAnimationTicks = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updatePreviewSelection(Minecraft client) {
|
||||
if (!veinModeKey.isDown() || !(client.hitResult instanceof BlockHitResult blockHitResult) || blockHitResult.getType() == HitResult.Type.MISS) {
|
||||
previewSelection = List.of();
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos origin = blockHitResult.getBlockPos();
|
||||
BlockState originState = client.level.getBlockState(origin);
|
||||
previewSelection = VeinSelection.collectPreview(client.level, origin, originState, currentVeinPower);
|
||||
}
|
||||
|
||||
private static boolean shouldContinueMining(Minecraft client) {
|
||||
return veinModeKey.isDown()
|
||||
&& client.hitResult instanceof BlockHitResult blockHitResult
|
||||
&& blockHitResult.getType() != HitResult.Type.MISS
|
||||
&& activeMiningOrigin != null
|
||||
&& activeMiningOrigin.equals(blockHitResult.getBlockPos());
|
||||
}
|
||||
|
||||
private static void stopMining(Minecraft client) {
|
||||
if (activeMiningOrigin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendMiningPacket(client, false, activeMiningOrigin);
|
||||
activeMiningOrigin = null;
|
||||
activeMiningPower = currentVeinPower;
|
||||
miningAnimationTicks = 0;
|
||||
}
|
||||
|
||||
public static boolean onMouseScroll(double horizontalAmount, double verticalAmount) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
|
||||
if (client.player == null || client.screen != null || veinModeKey == null || !veinModeKey.isDown()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int direction = scrollDirection(horizontalAmount, verticalAmount);
|
||||
|
||||
if (direction == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int nextPower = MinerConfig.clampVeinPower(currentVeinPower + direction * MinerConfig.VEIN_POWER_SCROLL_STEP);
|
||||
|
||||
if (nextPower == currentVeinPower) {
|
||||
showVeinPowerMessage(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
currentVeinPower = nextPower;
|
||||
showVeinPowerMessage(client);
|
||||
updatePreviewSelection(client);
|
||||
|
||||
if (activeMiningOrigin != null) {
|
||||
sendMiningPacket(client, true, activeMiningOrigin);
|
||||
activeMiningPower = currentVeinPower;
|
||||
miningAnimationTicks = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldReplaceCrosshair() {
|
||||
return veinModeKey != null && veinModeKey.isDown() && !previewSelection.isEmpty();
|
||||
}
|
||||
|
||||
public static Component crosshairReplacementText() {
|
||||
if (!shouldReplaceCrosshair()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Component.literal(Integer.toString(previewSelection.size()));
|
||||
}
|
||||
|
||||
private static void sendMiningPacket(Minecraft client, boolean active, BlockPos origin) {
|
||||
if (client.getConnection() != null) {
|
||||
ClientPlayNetworking.send(new MinerMiningPayload(active, origin, currentVeinPower));
|
||||
}
|
||||
}
|
||||
|
||||
private static void playMiningHitSound(Minecraft client, BlockPos pos) {
|
||||
if (client.level == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockState state = client.level.getBlockState(pos);
|
||||
|
||||
if (state.isAir()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SoundType soundType = state.getSoundType();
|
||||
client.level.playLocalSound(
|
||||
pos,
|
||||
soundType.getHitSound(),
|
||||
SoundSource.PLAYERS,
|
||||
HIT_SOUND_VOLUME,
|
||||
soundType.getPitch() * HIT_SOUND_PITCH_MULTIPLIER,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static int scrollDirection(double horizontalAmount, double verticalAmount) {
|
||||
if (verticalAmount > 0.0D) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verticalAmount < 0.0D) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (horizontalAmount > 0.0D) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (horizontalAmount < 0.0D) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void showVeinPowerMessage(Minecraft client) {
|
||||
client.player.sendOverlayMessage(Component.translatable("message.miner.vein_power", currentVeinPower));
|
||||
}
|
||||
|
||||
private static void renderSelectionCount(GuiGraphicsExtractor graphics, net.minecraft.client.DeltaTracker deltaTracker) {
|
||||
Component selectionText = crosshairReplacementText();
|
||||
|
||||
if (selectionText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Font font = Minecraft.getInstance().font;
|
||||
int x = (graphics.guiWidth() - font.width(selectionText)) / 2;
|
||||
int y = (graphics.guiHeight() - font.lineHeight) / 2;
|
||||
graphics.text(font, selectionText, x, y, 0xFFFFFFFF, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package fr.koka.miner.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gizmos.Gizmos;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class MinerPreviewRenderer {
|
||||
private static final int OUTLINE_COLOR = 0x4B000000;
|
||||
private static final double FACE_THICKNESS = 0.025D;
|
||||
private static final double OUTLINE_OFFSET = 0.002D;
|
||||
private static final float OUTLINE_WIDTH = 1.08F;
|
||||
|
||||
private MinerPreviewRenderer() {
|
||||
}
|
||||
|
||||
public static void collect(Minecraft client, List<BlockPos> selection) {
|
||||
if (selection.isEmpty() || client.level == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewMesh previewMesh = buildPreviewMesh(selection);
|
||||
|
||||
try (Gizmos.TemporaryCollection ignored = client.collectPerTickGizmos()) {
|
||||
for (LineSegment segment : previewMesh.outlineSegments()) {
|
||||
Gizmos.line(segment.start(), segment.end(), OUTLINE_COLOR, OUTLINE_WIDTH)
|
||||
.setAlwaysOnTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PreviewMesh buildPreviewMesh(List<BlockPos> selection) {
|
||||
Set<BlockPos> blocks = new HashSet<>();
|
||||
|
||||
for (BlockPos pos : selection) {
|
||||
blocks.add(pos.immutable());
|
||||
}
|
||||
|
||||
Map<Direction, Map<Integer, Set<FaceCell>>> facesByDirection = new EnumMap<>(Direction.class);
|
||||
|
||||
for (Direction direction : Direction.values()) {
|
||||
facesByDirection.put(direction, new java.util.HashMap<>());
|
||||
}
|
||||
|
||||
for (BlockPos pos : blocks) {
|
||||
for (Direction direction : Direction.values()) {
|
||||
if (blocks.contains(pos.relative(direction))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int plane = planeOf(pos, direction);
|
||||
facesByDirection.get(direction)
|
||||
.computeIfAbsent(plane, ignored -> new HashSet<>())
|
||||
.add(faceCellOf(pos, direction));
|
||||
}
|
||||
}
|
||||
|
||||
List<AABB> faceBoxes = new ArrayList<>();
|
||||
List<LineSegment> outlineSegments = new ArrayList<>();
|
||||
|
||||
for (Direction direction : Direction.values()) {
|
||||
for (Map.Entry<Integer, Set<FaceCell>> entry : facesByDirection.get(direction).entrySet()) {
|
||||
faceBoxes.addAll(mergeFacePlane(direction, entry.getKey(), entry.getValue()));
|
||||
outlineSegments.addAll(outlineSegmentsForPlane(direction, entry.getKey(), entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
return new PreviewMesh(faceBoxes, outlineSegments);
|
||||
}
|
||||
|
||||
private static List<AABB> mergeFacePlane(Direction direction, int plane, Set<FaceCell> faceCells) {
|
||||
Set<FaceCell> remaining = new HashSet<>(faceCells);
|
||||
List<AABB> boxes = new ArrayList<>();
|
||||
|
||||
while (!remaining.isEmpty()) {
|
||||
FaceCell start = pickLowest(remaining);
|
||||
int maxU = extendU(remaining, start);
|
||||
int maxV = extendV(remaining, start, maxU);
|
||||
|
||||
for (int u = start.u(); u <= maxU; u++) {
|
||||
for (int v = start.v(); v <= maxV; v++) {
|
||||
remaining.remove(new FaceCell(u, v));
|
||||
}
|
||||
}
|
||||
|
||||
boxes.add(faceBox(direction, plane, start.u(), start.v(), maxU + 1, maxV + 1));
|
||||
}
|
||||
|
||||
return boxes;
|
||||
}
|
||||
|
||||
private static List<LineSegment> outlineSegmentsForPlane(Direction direction, int plane, Set<FaceCell> faceCells) {
|
||||
Set<FaceEdge> edges = new HashSet<>();
|
||||
|
||||
for (FaceCell cell : faceCells) {
|
||||
toggleEdge(edges, FaceEdge.of(cell.u(), cell.v(), cell.u() + 1, cell.v()));
|
||||
toggleEdge(edges, FaceEdge.of(cell.u() + 1, cell.v(), cell.u() + 1, cell.v() + 1));
|
||||
toggleEdge(edges, FaceEdge.of(cell.u() + 1, cell.v() + 1, cell.u(), cell.v() + 1));
|
||||
toggleEdge(edges, FaceEdge.of(cell.u(), cell.v() + 1, cell.u(), cell.v()));
|
||||
}
|
||||
|
||||
List<LineSegment> segments = new ArrayList<>();
|
||||
|
||||
for (FaceEdge edge : edges) {
|
||||
segments.add(new LineSegment(
|
||||
facePoint(direction, plane, edge.u1(), edge.v1()),
|
||||
facePoint(direction, plane, edge.u2(), edge.v2())
|
||||
));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static void toggleEdge(Set<FaceEdge> edges, FaceEdge edge) {
|
||||
if (!edges.add(edge)) {
|
||||
edges.remove(edge);
|
||||
}
|
||||
}
|
||||
|
||||
private static FaceCell pickLowest(Set<FaceCell> cells) {
|
||||
FaceCell best = null;
|
||||
|
||||
for (FaceCell cell : cells) {
|
||||
if (best == null || cell.v() < best.v() || (cell.v() == best.v() && cell.u() < best.u())) {
|
||||
best = cell;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int extendU(Set<FaceCell> cells, FaceCell start) {
|
||||
int maxU = start.u();
|
||||
|
||||
while (cells.contains(new FaceCell(maxU + 1, start.v()))) {
|
||||
maxU++;
|
||||
}
|
||||
|
||||
return maxU;
|
||||
}
|
||||
|
||||
private static int extendV(Set<FaceCell> cells, FaceCell start, int maxU) {
|
||||
int maxV = start.v();
|
||||
|
||||
while (canExtendV(cells, start, maxU, maxV + 1)) {
|
||||
maxV++;
|
||||
}
|
||||
|
||||
return maxV;
|
||||
}
|
||||
|
||||
private static boolean canExtendV(Set<FaceCell> cells, FaceCell start, int maxU, int nextV) {
|
||||
for (int u = start.u(); u <= maxU; u++) {
|
||||
if (!cells.contains(new FaceCell(u, nextV))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int planeOf(BlockPos pos, Direction direction) {
|
||||
return switch (direction) {
|
||||
case WEST -> pos.getX();
|
||||
case EAST -> pos.getX() + 1;
|
||||
case DOWN -> pos.getY();
|
||||
case UP -> pos.getY() + 1;
|
||||
case NORTH -> pos.getZ();
|
||||
case SOUTH -> pos.getZ() + 1;
|
||||
};
|
||||
}
|
||||
|
||||
private static FaceCell faceCellOf(BlockPos pos, Direction direction) {
|
||||
return switch (direction) {
|
||||
case WEST, EAST -> new FaceCell(pos.getY(), pos.getZ());
|
||||
case DOWN, UP -> new FaceCell(pos.getX(), pos.getZ());
|
||||
case NORTH, SOUTH -> new FaceCell(pos.getX(), pos.getY());
|
||||
};
|
||||
}
|
||||
|
||||
private static AABB faceBox(Direction direction, int plane, int minU, int minV, int maxUExclusive, int maxVExclusive) {
|
||||
return switch (direction) {
|
||||
case WEST -> new AABB(plane, minU, minV, plane + FACE_THICKNESS, maxUExclusive, maxVExclusive);
|
||||
case EAST -> new AABB(plane - FACE_THICKNESS, minU, minV, plane, maxUExclusive, maxVExclusive);
|
||||
case DOWN -> new AABB(minU, plane, minV, maxUExclusive, plane + FACE_THICKNESS, maxVExclusive);
|
||||
case UP -> new AABB(minU, plane - FACE_THICKNESS, minV, maxUExclusive, plane, maxVExclusive);
|
||||
case NORTH -> new AABB(minU, minV, plane, maxUExclusive, maxVExclusive, plane + FACE_THICKNESS);
|
||||
case SOUTH -> new AABB(minU, minV, plane - FACE_THICKNESS, maxUExclusive, maxVExclusive, plane);
|
||||
};
|
||||
}
|
||||
|
||||
private static Vec3 facePoint(Direction direction, int plane, int u, int v) {
|
||||
double offsetX = direction.getStepX() * OUTLINE_OFFSET;
|
||||
double offsetY = direction.getStepY() * OUTLINE_OFFSET;
|
||||
double offsetZ = direction.getStepZ() * OUTLINE_OFFSET;
|
||||
|
||||
return switch (direction) {
|
||||
case WEST, EAST -> new Vec3(plane + offsetX, u + offsetY, v + offsetZ);
|
||||
case DOWN, UP -> new Vec3(u + offsetX, plane + offsetY, v + offsetZ);
|
||||
case NORTH, SOUTH -> new Vec3(u + offsetX, v + offsetY, plane + offsetZ);
|
||||
};
|
||||
}
|
||||
|
||||
private record FaceCell(int u, int v) {
|
||||
}
|
||||
|
||||
private record FaceEdge(int u1, int v1, int u2, int v2) {
|
||||
private static FaceEdge of(int u1, int v1, int u2, int v2) {
|
||||
if (u1 < u2 || (u1 == u2 && v1 <= v2)) {
|
||||
return new FaceEdge(u1, v1, u2, v2);
|
||||
}
|
||||
|
||||
return new FaceEdge(u2, v2, u1, v1);
|
||||
}
|
||||
}
|
||||
|
||||
private record LineSegment(Vec3 start, Vec3 end) {
|
||||
}
|
||||
|
||||
private record PreviewMesh(List<AABB> faceBoxes, List<LineSegment> outlineSegments) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.miner.client.mixin;
|
||||
|
||||
import fr.koka.miner.client.MinerClientMod;
|
||||
import net.minecraft.client.MouseHandler;
|
||||
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(MouseHandler.class)
|
||||
public abstract class MouseHandlerMixin {
|
||||
@Inject(method = "onScroll", at = @At("HEAD"), cancellable = true)
|
||||
private void miner$onScroll(long windowPointer, double horizontalAmount, double verticalAmount, CallbackInfo ci) {
|
||||
if (MinerClientMod.onMouseScroll(horizontalAmount, verticalAmount)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package fr.koka.view;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.view.config.ViewConfig;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class ViewClient implements ClientModInitializer {
|
||||
public static final String MOD_ID = "view";
|
||||
|
||||
private static final KeyMapping VIEW_KEY = KeyMappingHelper.registerKeyMapping(
|
||||
new KeyMapping("key.view.activate", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_V, KeyMapping.Category.MISC)
|
||||
);
|
||||
|
||||
private static ViewConfig config;
|
||||
private static double activeFov;
|
||||
private static boolean wasViewHeld;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
config = ViewConfig.load();
|
||||
activeFov = config.telephotoFov();
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(ViewClient::onEndClientTick);
|
||||
}
|
||||
|
||||
private static void onEndClientTick(Minecraft client) {
|
||||
boolean isViewHeld = isViewActive(client);
|
||||
if (isViewHeld && !wasViewHeld) {
|
||||
activeFov = config.telephotoFov();
|
||||
showCurrentFov(client);
|
||||
}
|
||||
|
||||
wasViewHeld = isViewHeld;
|
||||
}
|
||||
|
||||
public static boolean handleMouseScroll(double vertical) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (!isViewActive(client) || vertical == 0.0D) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double direction = Math.signum(vertical);
|
||||
if (config.invertScroll()) {
|
||||
direction = -direction;
|
||||
}
|
||||
|
||||
int notches = Math.max(1, (int) Math.round(Math.abs(vertical)));
|
||||
activeFov = config.clamp(activeFov + (direction * config.scrollStep() * notches));
|
||||
showCurrentFov(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static float resolveFov(float originalFov) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (!isViewActive(client)) {
|
||||
return originalFov;
|
||||
}
|
||||
|
||||
return (float) config.clamp(activeFov);
|
||||
}
|
||||
|
||||
private static boolean isViewActive(Minecraft client) {
|
||||
return client != null
|
||||
&& client.player != null
|
||||
&& client.screen == null
|
||||
&& VIEW_KEY.isDown();
|
||||
}
|
||||
|
||||
private static void showCurrentFov(Minecraft client) {
|
||||
if (client == null || client.player == null || !config.showActionBarValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.player.sendOverlayMessage(
|
||||
Component.translatable("message.view.fov", String.format(Locale.ROOT, "%.1f", activeFov))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package fr.koka.view.mixin;
|
||||
|
||||
import fr.koka.view.ViewClient;
|
||||
import net.minecraft.client.Camera;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(Camera.class)
|
||||
abstract class GameRendererMixin {
|
||||
@Inject(method = "calculateFov", at = @At("RETURN"), cancellable = true)
|
||||
private void view$overrideHeldFov(
|
||||
float tickProgress,
|
||||
CallbackInfoReturnable<Float> cir
|
||||
) {
|
||||
cir.setReturnValue(ViewClient.resolveFov(cir.getReturnValue()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.view.mixin;
|
||||
|
||||
import fr.koka.view.ViewClient;
|
||||
import net.minecraft.client.MouseHandler;
|
||||
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(MouseHandler.class)
|
||||
abstract class MouseMixin {
|
||||
@Inject(method = "onScroll", at = @At("HEAD"), cancellable = true)
|
||||
private void view$consumeScrollWhileHeld(long window, double horizontal, double vertical, CallbackInfo ci) {
|
||||
if (ViewClient.handleMouseScroll(vertical)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package fr.koka99cab.minecrafter.client;
|
||||
|
||||
import com.koka.heartoverlay.HeartOverlayClient;
|
||||
import dev.koka.previewer.PreviewerClient;
|
||||
import fr.koka.builder.client.BuilderClientMod;
|
||||
import fr.koka.miner.client.MinerClientMod;
|
||||
import fr.koka.view.ViewClient;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import fr.koka99cab.minecrafter.MinecrafterMod;
|
||||
|
||||
public class MinecrafterClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
new BuilderClientMod().onInitializeClient();
|
||||
new MinerClientMod().onInitializeClient();
|
||||
new PreviewerClient().onInitializeClient();
|
||||
new ViewClient().onInitializeClient();
|
||||
new HeartOverlayClient().onInitializeClient();
|
||||
MinecrafterMod.LOGGER.info("Minecrafter client initialise.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "fr.koka.builder.client.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"client": [
|
||||
"MinecraftMixin",
|
||||
"MouseHandlerMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "fr.koka.miner.client.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"client": [
|
||||
"MouseHandlerMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "fr.koka.view.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"client": [
|
||||
"GameRendererMixin",
|
||||
"MouseMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package fr.koka.builder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class BuildSelection {
|
||||
private BuildSelection() {
|
||||
}
|
||||
|
||||
public static boolean canUseBuilderItem(Player player) {
|
||||
return !player.getMainHandItem().isEmpty() && player.getMainHandItem().getItem() instanceof BlockItem;
|
||||
}
|
||||
|
||||
public static List<BlockPos> collectPreview(Player player, Level level, BlockHitResult hitResult, int buildPower) {
|
||||
return collectPreview(player, level, hitResult.getBlockPos(), hitResult.getDirection(), localHitOffset(hitResult), buildPower);
|
||||
}
|
||||
|
||||
public static List<BlockPos> collectPreview(Player player, Level level, BlockPos origin, Direction face, Vec3 hitOffset, int buildPower) {
|
||||
BlockPlaceContext baseContext = createBaseContext(player, origin, face, hitOffset);
|
||||
|
||||
if (baseContext == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return collect(level, baseContext, face, BuilderConfig.buildRadius(buildPower));
|
||||
}
|
||||
|
||||
public static boolean tryPlace(ServerPlayer player, BlockPos origin, Direction face, Vec3 hitOffset, BlockPos targetPos) {
|
||||
BlockPlaceContext baseContext = createBaseContext(player, origin, face, hitOffset);
|
||||
|
||||
if (baseContext == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlockPlaceContext targetContext = createTargetContext(baseContext, targetPos, face);
|
||||
|
||||
if (targetContext == null || !targetContext.canPlace()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlockPos supportPos = targetPos.relative(face.getOpposite());
|
||||
BlockHitResult placementHitResult = new BlockHitResult(
|
||||
Vec3.atLowerCornerOf(supportPos).add(hitOffset),
|
||||
face,
|
||||
supportPos,
|
||||
false
|
||||
);
|
||||
|
||||
InteractionResult result = player.gameMode.useItemOn(
|
||||
player,
|
||||
player.level(),
|
||||
player.getMainHandItem(),
|
||||
InteractionHand.MAIN_HAND,
|
||||
placementHitResult
|
||||
);
|
||||
return result.consumesAction();
|
||||
}
|
||||
|
||||
public static Vec3 localHitOffset(BlockHitResult hitResult) {
|
||||
return hitResult.getLocation().subtract(Vec3.atLowerCornerOf(hitResult.getBlockPos()));
|
||||
}
|
||||
|
||||
private static List<BlockPos> collect(Level level, BlockPlaceContext baseContext, Direction face, int radius) {
|
||||
BlockPos anchor = baseContext.getClickedPos();
|
||||
Direction horizontal = horizontalAxis(face);
|
||||
Direction vertical = verticalAxis(face);
|
||||
List<BlockPos> selection = new ArrayList<>();
|
||||
|
||||
for (PlaneOffset offset : orderedOffsets(radius)) {
|
||||
BlockPos targetPos = anchor.relative(horizontal, offset.horizontal()).relative(vertical, offset.vertical());
|
||||
BlockPlaceContext targetContext = createTargetContext(baseContext, targetPos, face);
|
||||
|
||||
if (targetContext == null || !targetContext.canPlace()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
selection.add(targetPos.immutable());
|
||||
}
|
||||
|
||||
return List.copyOf(selection);
|
||||
}
|
||||
|
||||
private static BlockPlaceContext createBaseContext(Player player, BlockPos origin, Direction face, Vec3 hitOffset) {
|
||||
if (player == null || !canUseBuilderItem(player)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockHitResult hitResult = new BlockHitResult(
|
||||
Vec3.atLowerCornerOf(origin).add(hitOffset),
|
||||
face,
|
||||
origin,
|
||||
false
|
||||
);
|
||||
return new BlockPlaceContext(player, InteractionHand.MAIN_HAND, player.getMainHandItem(), hitResult);
|
||||
}
|
||||
|
||||
private static BlockPlaceContext createTargetContext(BlockPlaceContext baseContext, BlockPos targetPos, Direction face) {
|
||||
Level level = baseContext.getLevel();
|
||||
BlockPos supportPos = targetPos.relative(face.getOpposite());
|
||||
|
||||
if (!level.isInWorldBounds(targetPos) || !level.isInWorldBounds(supportPos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (level.getBlockState(supportPos).isAir()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockPlaceContext targetContext = BlockPlaceContext.at(baseContext, supportPos, face);
|
||||
|
||||
if (!targetContext.getClickedPos().equals(targetPos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return targetContext;
|
||||
}
|
||||
|
||||
private static Direction horizontalAxis(Direction face) {
|
||||
return switch (face.getAxis()) {
|
||||
case X -> Direction.SOUTH;
|
||||
case Y, Z -> Direction.EAST;
|
||||
};
|
||||
}
|
||||
|
||||
private static Direction verticalAxis(Direction face) {
|
||||
return switch (face.getAxis()) {
|
||||
case X, Z -> Direction.UP;
|
||||
case Y -> Direction.SOUTH;
|
||||
};
|
||||
}
|
||||
|
||||
private static List<PlaneOffset> orderedOffsets(int radius) {
|
||||
List<PlaneOffset> offsets = new ArrayList<>();
|
||||
|
||||
for (int vertical = -radius; vertical <= radius; vertical++) {
|
||||
for (int horizontal = -radius; horizontal <= radius; horizontal++) {
|
||||
offsets.add(new PlaneOffset(horizontal, vertical));
|
||||
}
|
||||
}
|
||||
|
||||
offsets.sort(
|
||||
Comparator
|
||||
.comparingInt((PlaneOffset offset) -> Math.max(Math.abs(offset.horizontal()), Math.abs(offset.vertical())))
|
||||
.thenComparingInt(offset -> Math.abs(offset.horizontal()) + Math.abs(offset.vertical()))
|
||||
.thenComparingInt(offset -> Math.abs(offset.vertical()))
|
||||
.thenComparingInt(PlaneOffset::vertical)
|
||||
.thenComparingInt(PlaneOffset::horizontal)
|
||||
);
|
||||
return offsets;
|
||||
}
|
||||
|
||||
private record PlaneOffset(int horizontal, int vertical) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fr.koka.builder;
|
||||
|
||||
public final class BuilderConfig {
|
||||
public static final int MIN_BUILD_POWER = 1;
|
||||
public static final int DEFAULT_BUILD_POWER = 2;
|
||||
public static final int MAX_BUILD_POWER = 6;
|
||||
public static final int MAX_BUILD_DIAMETER = MAX_BUILD_POWER * 2 - 1;
|
||||
public static final int BUILD_POWER_SCROLL_STEP = 1;
|
||||
public static final float SURVIVAL_BUILD_PROGRESS_PER_TICK = 0.5F;
|
||||
|
||||
private BuilderConfig() {
|
||||
}
|
||||
|
||||
public static int clampBuildPower(int power) {
|
||||
return Math.max(MIN_BUILD_POWER, Math.min(MAX_BUILD_POWER, power));
|
||||
}
|
||||
|
||||
public static int buildRadius(int power) {
|
||||
return clampBuildPower(power) - 1;
|
||||
}
|
||||
|
||||
public static int buildDiameter(int power) {
|
||||
return buildRadius(power) * 2 + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package fr.koka.builder;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public record BuilderControlPayload(boolean active, BlockPos origin, Direction face, Vec3 hitOffset, int power) implements CustomPacketPayload {
|
||||
public static final CustomPacketPayload.Type<BuilderControlPayload> ID = new CustomPacketPayload.Type<>(BuilderMod.id("build_control"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, BuilderControlPayload> CODEC = CustomPacketPayload.codec(BuilderControlPayload::write, BuilderControlPayload::new);
|
||||
|
||||
public BuilderControlPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this(
|
||||
buffer.readBoolean(),
|
||||
buffer.readBlockPos(),
|
||||
Direction.from3DDataValue(buffer.readVarInt()),
|
||||
new Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble()),
|
||||
BuilderConfig.clampBuildPower(buffer.readVarInt())
|
||||
);
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeBoolean(this.active);
|
||||
buffer.writeBlockPos(this.origin);
|
||||
buffer.writeVarInt(this.face.get3DDataValue());
|
||||
buffer.writeDouble(this.hitOffset.x);
|
||||
buffer.writeDouble(this.hitOffset.y);
|
||||
buffer.writeDouble(this.hitOffset.z);
|
||||
buffer.writeVarInt(this.power);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.builder;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class BuilderMod implements ModInitializer {
|
||||
public static final String MOD_ID = "builder";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public static Identifier id(String path) {
|
||||
return Identifier.fromNamespaceAndPath(MOD_ID, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
BuilderPlacementManager.register();
|
||||
LOGGER.info(
|
||||
"Builder ready. defaultBuildPower={}, maxBuildPower={}, maxDiameter={}",
|
||||
BuilderConfig.DEFAULT_BUILD_POWER,
|
||||
BuilderConfig.MAX_BUILD_POWER,
|
||||
BuilderConfig.MAX_BUILD_DIAMETER
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package fr.koka.builder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class BuilderPlacementManager {
|
||||
private static final Map<UUID, GroupBuildingSession> SESSIONS = new HashMap<>();
|
||||
|
||||
private BuilderPlacementManager() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
PayloadTypeRegistry.serverboundPlay().register(BuilderControlPayload.ID, BuilderControlPayload.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(BuilderControlPayload.ID, (payload, context) -> {
|
||||
if (payload.active()) {
|
||||
startOrUpdateSession(context.player(), payload.origin(), payload.face(), payload.hitOffset(), payload.power());
|
||||
} else {
|
||||
stopSession(context.player().getUUID());
|
||||
}
|
||||
});
|
||||
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
|
||||
stopSession(handler.player.getUUID());
|
||||
});
|
||||
|
||||
ServerTickEvents.END_SERVER_TICK.register(server -> tickSessions());
|
||||
}
|
||||
|
||||
private static void startOrUpdateSession(ServerPlayer player, BlockPos origin, Direction face, Vec3 hitOffset, int buildPower) {
|
||||
GroupBuildingSession currentSession = SESSIONS.get(player.getUUID());
|
||||
|
||||
if (currentSession != null && currentSession.matches(origin, face, buildPower)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopSession(player.getUUID());
|
||||
|
||||
GroupBuildingSession nextSession = GroupBuildingSession.create(player, origin, face, hitOffset, buildPower);
|
||||
|
||||
if (nextSession == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.isCreative()) {
|
||||
nextSession.buildInstantly();
|
||||
} else {
|
||||
SESSIONS.put(player.getUUID(), nextSession);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tickSessions() {
|
||||
Iterator<Map.Entry<UUID, GroupBuildingSession>> iterator = SESSIONS.entrySet().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<UUID, GroupBuildingSession> entry = iterator.next();
|
||||
|
||||
if (!entry.getValue().tick()) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void stopSession(UUID playerId) {
|
||||
GroupBuildingSession session = SESSIONS.remove(playerId);
|
||||
|
||||
if (session != null) {
|
||||
session.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package fr.koka.builder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class GroupBuildingSession {
|
||||
private final ServerPlayer player;
|
||||
private final ServerLevel level;
|
||||
private final BlockPos origin;
|
||||
private final Direction face;
|
||||
private final Vec3 hitOffset;
|
||||
private final int buildPower;
|
||||
private final List<BlockPos> selection;
|
||||
private float placementProgress;
|
||||
private int nextIndex;
|
||||
|
||||
private GroupBuildingSession(ServerPlayer player, ServerLevel level, BlockPos origin, Direction face, Vec3 hitOffset, int buildPower, List<BlockPos> selection) {
|
||||
this.player = player;
|
||||
this.level = level;
|
||||
this.origin = origin;
|
||||
this.face = face;
|
||||
this.hitOffset = hitOffset;
|
||||
this.buildPower = buildPower;
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
public static GroupBuildingSession create(ServerPlayer player, BlockPos origin, Direction face, Vec3 hitOffset, int buildPower) {
|
||||
ServerLevel level = (ServerLevel) player.level();
|
||||
int clampedPower = BuilderConfig.clampBuildPower(buildPower);
|
||||
List<BlockPos> selection = BuildSelection.collectPreview(player, level, origin, face, hitOffset, clampedPower);
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GroupBuildingSession(player, level, origin, face, hitOffset, clampedPower, selection);
|
||||
}
|
||||
|
||||
public boolean matches(BlockPos blockPos, Direction requestedFace, int requestedBuildPower) {
|
||||
return this.origin.equals(blockPos)
|
||||
&& this.face == requestedFace
|
||||
&& this.buildPower == BuilderConfig.clampBuildPower(requestedBuildPower);
|
||||
}
|
||||
|
||||
public void buildInstantly() {
|
||||
while (advanceOnce()) {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean tick() {
|
||||
if (!this.player.isAlive() || this.player.level() != this.level) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BuildSelection.canUseBuilderItem(this.player) || this.nextIndex >= this.selection.size()) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.placementProgress += BuilderConfig.SURVIVAL_BUILD_PROGRESS_PER_TICK;
|
||||
|
||||
while (this.placementProgress >= 1.0F) {
|
||||
this.placementProgress -= 1.0F;
|
||||
|
||||
if (!advanceOnce()) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
}
|
||||
|
||||
private boolean advanceOnce() {
|
||||
while (this.nextIndex < this.selection.size()) {
|
||||
BlockPos targetPos = this.selection.get(this.nextIndex++);
|
||||
|
||||
if (BuildSelection.tryPlace(this.player, this.origin, this.face, this.hitOffset, targetPos)) {
|
||||
return this.nextIndex < this.selection.size();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package fr.koka.miner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.BlockParticleOption;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class GroupMiningSession {
|
||||
private static final int DESTROY_PARTICLE_COUNT = 12;
|
||||
private static final double DESTROY_PARTICLE_SPREAD = 0.35D;
|
||||
private static final double DESTROY_PARTICLE_SPEED = 0.05D;
|
||||
private static final float MIN_DESTROY_SOUND_VOLUME = 0.8F;
|
||||
private static final float DESTROY_SOUND_VOLUME_MULTIPLIER = 0.6F;
|
||||
private static final float DESTROY_SOUND_MIN_PITCH_MULTIPLIER = 0.85F;
|
||||
private static final float DESTROY_SOUND_PITCH_VARIANCE = 0.25F;
|
||||
private final ServerPlayer player;
|
||||
private final ServerLevel level;
|
||||
private final BlockPos origin;
|
||||
private final BlockState originState;
|
||||
private final Block targetBlock;
|
||||
private final int veinPower;
|
||||
private final List<BlockPos> selection;
|
||||
private final float requiredProgress;
|
||||
private float progress;
|
||||
|
||||
private GroupMiningSession(ServerPlayer player, ServerLevel level, BlockPos origin, BlockState originState, int veinPower, List<BlockPos> selection) {
|
||||
this.player = player;
|
||||
this.level = level;
|
||||
this.origin = origin;
|
||||
this.originState = originState;
|
||||
this.targetBlock = originState.getBlock();
|
||||
this.veinPower = veinPower;
|
||||
this.selection = selection;
|
||||
this.requiredProgress = Math.max(1.0F, selection.size());
|
||||
}
|
||||
|
||||
public static GroupMiningSession create(ServerPlayer player, BlockPos origin, int veinPower) {
|
||||
ServerLevel level = (ServerLevel) player.level();
|
||||
BlockState originState = level.getBlockState(origin);
|
||||
int clampedPower = MinerConfig.clampVeinPower(veinPower);
|
||||
List<BlockPos> selection = VeinSelection.collectPreview(level, origin, originState, clampedPower);
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GroupMiningSession(player, level, origin, originState, clampedPower, selection);
|
||||
}
|
||||
|
||||
public boolean matches(BlockPos blockPos, int requestedVeinPower) {
|
||||
return this.origin.equals(blockPos) && this.veinPower == MinerConfig.clampVeinPower(requestedVeinPower);
|
||||
}
|
||||
|
||||
public void breakInstantly() {
|
||||
complete();
|
||||
}
|
||||
|
||||
public boolean tick() {
|
||||
if (!this.player.isAlive() || this.player.level() != this.level) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.level.getBlockState(this.origin).is(this.targetBlock)) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
float destroyDelta = this.originState.getDestroyProgress(this.player, this.level, this.origin);
|
||||
|
||||
if (destroyDelta <= 0.0F) {
|
||||
updateCracks(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.progress = Math.min(this.requiredProgress, this.progress + destroyDelta);
|
||||
float normalizedProgress = this.progress / this.requiredProgress;
|
||||
int stage = Math.min(9, (int) (normalizedProgress * 10.0F));
|
||||
updateCracks(stage);
|
||||
|
||||
if (this.progress >= this.requiredProgress) {
|
||||
complete();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
updateCracks(-1);
|
||||
}
|
||||
|
||||
private void complete() {
|
||||
updateCracks(-1);
|
||||
|
||||
for (BlockPos pos : this.selection) {
|
||||
BlockState state = this.level.getBlockState(pos);
|
||||
|
||||
if (state.is(this.targetBlock)
|
||||
&& this.player.gameMode.destroyBlock(pos)
|
||||
&& !this.level.getBlockState(pos).is(this.targetBlock)) {
|
||||
playDestroyEffect(pos, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void playDestroyEffect(BlockPos pos, BlockState state) {
|
||||
SoundType soundType = state.getSoundType();
|
||||
float volume = Math.max(MIN_DESTROY_SOUND_VOLUME, (soundType.getVolume() + 1.0F) * DESTROY_SOUND_VOLUME_MULTIPLIER);
|
||||
float pitch = soundType.getPitch() * (DESTROY_SOUND_MIN_PITCH_MULTIPLIER + this.level.getRandom().nextFloat() * DESTROY_SOUND_PITCH_VARIANCE);
|
||||
|
||||
this.level.playSound(null, pos, soundType.getBreakSound(), SoundSource.BLOCKS, volume, pitch);
|
||||
this.level.sendParticles(
|
||||
new BlockParticleOption(ParticleTypes.BLOCK, state),
|
||||
pos.getX() + 0.5D,
|
||||
pos.getY() + 0.5D,
|
||||
pos.getZ() + 0.5D,
|
||||
DESTROY_PARTICLE_COUNT,
|
||||
DESTROY_PARTICLE_SPREAD,
|
||||
DESTROY_PARTICLE_SPREAD,
|
||||
DESTROY_PARTICLE_SPREAD,
|
||||
DESTROY_PARTICLE_SPEED
|
||||
);
|
||||
}
|
||||
|
||||
private void updateCracks(int stage) {
|
||||
for (int index = 0; index < this.selection.size(); index++) {
|
||||
BlockPos pos = this.selection.get(index);
|
||||
int displayStage = this.level.getBlockState(pos).is(this.targetBlock) ? stage : -1;
|
||||
this.level.destroyBlockProgress(progressId(index), pos, displayStage);
|
||||
}
|
||||
}
|
||||
|
||||
private int progressId(int index) {
|
||||
return 5_000_000 + this.player.getId() * MinerConfig.MAX_VEIN_POWER + index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka.miner;
|
||||
|
||||
public final class MinerConfig {
|
||||
public static final int SEARCH_RADIUS = 6;
|
||||
public static final int MIN_VEIN_POWER = 1;
|
||||
public static final int DEFAULT_VEIN_POWER = 12;
|
||||
public static final int MAX_VEIN_POWER = 256;
|
||||
public static final int VEIN_POWER_SCROLL_STEP = 1;
|
||||
|
||||
private MinerConfig() {
|
||||
}
|
||||
|
||||
public static int clampVeinPower(int power) {
|
||||
return Math.max(MIN_VEIN_POWER, Math.min(MAX_VEIN_POWER, power));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.miner;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
public record MinerMiningPayload(boolean active, BlockPos origin, int power) implements CustomPacketPayload {
|
||||
public static final CustomPacketPayload.Type<MinerMiningPayload> ID = new CustomPacketPayload.Type<>(MinerMod.id("mining_control"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, MinerMiningPayload> CODEC = CustomPacketPayload.codec(MinerMiningPayload::write, MinerMiningPayload::new);
|
||||
|
||||
public MinerMiningPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this(buffer.readBoolean(), buffer.readBlockPos(), MinerConfig.clampVeinPower(buffer.readVarInt()));
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeBoolean(this.active);
|
||||
buffer.writeBlockPos(this.origin);
|
||||
buffer.writeVarInt(this.power);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.miner;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class MinerMod implements ModInitializer {
|
||||
public static final String MOD_ID = "miner";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public static Identifier id(String path) {
|
||||
return Identifier.fromNamespaceAndPath(MOD_ID, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
VeinMiningManager.register();
|
||||
LOGGER.info(
|
||||
"Miner ready. Radius={}, defaultVeinPower={}, maxVeinPower={}",
|
||||
MinerConfig.SEARCH_RADIUS,
|
||||
MinerConfig.DEFAULT_VEIN_POWER,
|
||||
MinerConfig.MAX_VEIN_POWER
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package fr.koka.miner;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
public final class VeinMiningManager {
|
||||
private static final Map<UUID, GroupMiningSession> SESSIONS = new HashMap<>();
|
||||
|
||||
private VeinMiningManager() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
PayloadTypeRegistry.serverboundPlay().register(MinerMiningPayload.ID, MinerMiningPayload.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(MinerMiningPayload.ID, (payload, context) -> {
|
||||
if (payload.active()) {
|
||||
startOrUpdateSession(context.player(), payload.origin(), payload.power());
|
||||
} else {
|
||||
stopSession(context.player().getUUID());
|
||||
}
|
||||
});
|
||||
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
|
||||
stopSession(handler.player.getUUID());
|
||||
});
|
||||
|
||||
ServerTickEvents.END_SERVER_TICK.register(server -> tickSessions());
|
||||
}
|
||||
|
||||
private static void startOrUpdateSession(ServerPlayer player, net.minecraft.core.BlockPos origin, int veinPower) {
|
||||
GroupMiningSession currentSession = SESSIONS.get(player.getUUID());
|
||||
|
||||
if (currentSession != null && currentSession.matches(origin, veinPower)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopSession(player.getUUID());
|
||||
|
||||
GroupMiningSession nextSession = GroupMiningSession.create(player, origin, veinPower);
|
||||
|
||||
if (nextSession == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.isCreative()) {
|
||||
nextSession.breakInstantly();
|
||||
} else {
|
||||
SESSIONS.put(player.getUUID(), nextSession);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tickSessions() {
|
||||
Iterator<Map.Entry<UUID, GroupMiningSession>> iterator = SESSIONS.entrySet().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<UUID, GroupMiningSession> entry = iterator.next();
|
||||
|
||||
if (!entry.getValue().tick()) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void stopSession(UUID playerId) {
|
||||
GroupMiningSession session = SESSIONS.remove(playerId);
|
||||
|
||||
if (session != null) {
|
||||
session.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package fr.koka.miner;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class VeinSelection {
|
||||
private VeinSelection() {
|
||||
}
|
||||
|
||||
public static List<BlockPos> collectPreview(Level level, BlockPos origin, BlockState originState) {
|
||||
return collectPreview(level, origin, originState, MinerConfig.DEFAULT_VEIN_POWER);
|
||||
}
|
||||
|
||||
public static List<BlockPos> collectPreview(Level level, BlockPos origin, BlockState originState, int maxBlocks) {
|
||||
return collect(level, origin, originState, MinerConfig.SEARCH_RADIUS, MinerConfig.clampVeinPower(maxBlocks), true);
|
||||
}
|
||||
|
||||
public static List<BlockPos> collectAfterBreak(Level level, BlockPos origin, BlockState originState) {
|
||||
return collectAfterBreak(level, origin, originState, MinerConfig.DEFAULT_VEIN_POWER);
|
||||
}
|
||||
|
||||
public static List<BlockPos> collectAfterBreak(Level level, BlockPos origin, BlockState originState, int maxBlocks) {
|
||||
return collect(level, origin, originState, MinerConfig.SEARCH_RADIUS, Math.max(0, MinerConfig.clampVeinPower(maxBlocks) - 1), false);
|
||||
}
|
||||
|
||||
private static List<BlockPos> collect(Level level, BlockPos origin, BlockState originState, int radius, int limit, boolean includeOrigin) {
|
||||
if (limit <= 0 || originState.isAir() || originState.hasBlockEntity()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Block targetBlock = originState.getBlock();
|
||||
LinkedHashSet<BlockPos> selected = new LinkedHashSet<>();
|
||||
ArrayDeque<BlockPos> frontier = new ArrayDeque<>();
|
||||
|
||||
if (includeOrigin && matches(level, origin, targetBlock)) {
|
||||
selected.add(origin);
|
||||
frontier.add(origin);
|
||||
} else {
|
||||
seedNeighbors(level, origin, targetBlock, radius, limit, selected, frontier);
|
||||
}
|
||||
|
||||
while (!frontier.isEmpty() && selected.size() < limit) {
|
||||
BlockPos current = frontier.removeFirst();
|
||||
|
||||
for (Direction direction : Direction.values()) {
|
||||
BlockPos next = current.relative(direction);
|
||||
|
||||
if (!isWithinRadius(origin, next, radius) || selected.contains(next) || !matches(level, next, targetBlock)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
selected.add(next);
|
||||
frontier.addLast(next);
|
||||
|
||||
if (selected.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
private static void seedNeighbors(Level level, BlockPos origin, Block targetBlock, int radius, int limit, LinkedHashSet<BlockPos> selected, ArrayDeque<BlockPos> frontier) {
|
||||
for (Direction direction : Direction.values()) {
|
||||
if (selected.size() >= limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos next = origin.relative(direction);
|
||||
|
||||
if (!isWithinRadius(origin, next, radius) || !matches(level, next, targetBlock) || selected.contains(next)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
selected.add(next);
|
||||
frontier.addLast(next);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matches(Level level, BlockPos pos, Block targetBlock) {
|
||||
BlockState state = level.getBlockState(pos);
|
||||
return !state.isAir() && !state.hasBlockEntity() && state.is(targetBlock);
|
||||
}
|
||||
|
||||
private static boolean isWithinRadius(BlockPos origin, BlockPos candidate, int radius) {
|
||||
return Math.abs(candidate.getX() - origin.getX()) <= radius
|
||||
&& Math.abs(candidate.getY() - origin.getY()) <= radius
|
||||
&& Math.abs(candidate.getZ() - origin.getZ()) <= radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package fr.koka.view.config;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.util.Mth;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class ViewConfig {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("view");
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("view.json");
|
||||
|
||||
private double telephotoFov = 10.0D;
|
||||
private double quickProFov = 110.0D;
|
||||
private double scrollStep = 5.0D;
|
||||
private boolean invertScroll = false;
|
||||
private boolean showActionBarValue = true;
|
||||
|
||||
public static ViewConfig load() {
|
||||
ViewConfig config = null;
|
||||
|
||||
if (Files.exists(CONFIG_PATH)) {
|
||||
try (Reader reader = Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8)) {
|
||||
config = GSON.fromJson(reader, ViewConfig.class);
|
||||
} catch (IOException exception) {
|
||||
LOGGER.warn("Unable to read {}, using defaults.", CONFIG_PATH, exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
config = new ViewConfig();
|
||||
}
|
||||
|
||||
config.normalize();
|
||||
config.save();
|
||||
return config;
|
||||
}
|
||||
|
||||
public double telephotoFov() {
|
||||
return this.telephotoFov;
|
||||
}
|
||||
|
||||
public double quickProFov() {
|
||||
return this.quickProFov;
|
||||
}
|
||||
|
||||
public double scrollStep() {
|
||||
return this.scrollStep;
|
||||
}
|
||||
|
||||
public boolean invertScroll() {
|
||||
return this.invertScroll;
|
||||
}
|
||||
|
||||
public boolean showActionBarValue() {
|
||||
return this.showActionBarValue;
|
||||
}
|
||||
|
||||
public double clamp(double value) {
|
||||
return Mth.clamp(value, this.telephotoFov, this.quickProFov);
|
||||
}
|
||||
|
||||
private void normalize() {
|
||||
this.telephotoFov = Mth.clamp(this.telephotoFov, 1.0D, 170.0D);
|
||||
this.quickProFov = Mth.clamp(this.quickProFov, this.telephotoFov, 170.0D);
|
||||
this.scrollStep = Mth.clamp(this.scrollStep, 0.5D, 40.0D);
|
||||
}
|
||||
|
||||
private void save() {
|
||||
try {
|
||||
Files.createDirectories(CONFIG_PATH.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) {
|
||||
GSON.toJson(this, writer);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
LOGGER.warn("Unable to write {}", CONFIG_PATH, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.minecrafter;
|
||||
|
||||
import fr.koka.builder.BuilderMod;
|
||||
import fr.koka.miner.MinerMod;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MinecrafterMod implements ModInitializer {
|
||||
public static final String MOD_ID = "minecrafter";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
new BuilderMod().onInitialize();
|
||||
new MinerMod().onInitialize();
|
||||
LOGGER.info("Minecrafter charge pour Minecraft 26.1.2.");
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"key.categories.builder.builder": "Builder",
|
||||
"key.builder.build_mode": "Area Build",
|
||||
"message.builder.build_power": "Builder power: %s (diameter %s)"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"key.categories.builder.builder": "Builder",
|
||||
"key.builder.build_mode": "Build de zone",
|
||||
"message.builder.build_power": "Puissance builder : %s (diametre %s)"
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"modmenu.nameTranslation.minecrafter": "Minecrafter",
|
||||
"modmenu.descriptionTranslation.minecrafter": "Combines Builder, Miner, Previewer, View, and Heart Overlay into one Fabric gameplay mod."
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"modmenu.nameTranslation.minecrafter": "Minecrafter",
|
||||
"modmenu.descriptionTranslation.minecrafter": "Combine Builder, Miner, Previewer, View et Heart Overlay dans un seul mod de gameplay Fabric."
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"key.categories.miner.miner": "Miner",
|
||||
"key.miner.vein_mode": "Vein Mining Preview",
|
||||
"message.miner.vein_power": "Veinmine power: %s"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"key.categories.miner.miner": "Miner",
|
||||
"key.miner.vein_mode": "Apercu de veine",
|
||||
"message.miner.vein_power": "Puissance veinmine : %s"
|
||||
}
|
||||
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"key.category.view": "View",
|
||||
"key.view.activate": "View lens",
|
||||
"message.view.fov": "View FOV: %s"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"key.category.view": "View",
|
||||
"key.view.activate": "Lentille View",
|
||||
"message.view.fov": "FOV View : %s"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "minecrafter",
|
||||
"version": "${version}",
|
||||
"name": "Minecrafter",
|
||||
"description": "Combines Builder, Miner, Previewer, View, and Heart Overlay into one Fabric gameplay mod for Minecraft 26.1.2, adding area building, vein mining, placement previews, adjustable zoom, and mob health hearts.",
|
||||
"icon": "assets/minecrafter/icon.png",
|
||||
"authors": [
|
||||
"KOKA99CAB"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"fr.koka99cab.minecrafter.MinecrafterMod"
|
||||
],
|
||||
"client": [
|
||||
"fr.koka99cab.minecrafter.client.MinecrafterClient"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
{
|
||||
"config": "builder.client.mixins.json",
|
||||
"environment": "client"
|
||||
},
|
||||
{
|
||||
"config": "miner.client.mixins.json",
|
||||
"environment": "client"
|
||||
},
|
||||
{
|
||||
"config": "view.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.2",
|
||||
"minecraft": "~26.1.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": "*"
|
||||
}
|
||||
}
|
||||