Initial sanctuary sources

This commit is contained in:
cnstiout
2026-07-04 20:27:08 +02:00
commit c796a20321
1582 changed files with 253264 additions and 0 deletions
+34
View File
@@ -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
+67
View File
@@ -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 {
}
}
+21
View File
@@ -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
Binary file not shown.
@@ -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
Vendored Executable
+253
View File
@@ -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" "$@"
+93
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+12
View File
@@ -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
}
}
+12
View File
@@ -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.");
}
}
Binary file not shown.

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)"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

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."
}
Binary file not shown.

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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

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": "*"
}
}