Initial sanctuary sources
@@ -0,0 +1,72 @@
|
||||
plugins {
|
||||
id "net.fabricmc.fabric-loom" version "${loom_version}"
|
||||
id "maven-publish"
|
||||
}
|
||||
|
||||
version = project.mod_version
|
||||
group = project.maven_group
|
||||
|
||||
base {
|
||||
archivesName = project.archives_base_name
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
loom {
|
||||
splitEnvironmentSourceSets()
|
||||
|
||||
mods {
|
||||
"computer" {
|
||||
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 {
|
||||
inputs.property "archivesName", project.base.archivesName
|
||||
|
||||
from("../../license.txt") {
|
||||
rename { "${it}_${inputs.properties.archivesName}" }
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create("mavenJava", MavenPublication) {
|
||||
artifactId = project.archives_base_name
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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
|
||||
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=computer
|
||||
|
||||
# Dependencies
|
||||
fabric_api_version=0.148.0+26.1.2
|
||||
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
if [ -z "$JAVA_HOME" ] && [ -x "$APP_HOME/.jdk/current/bin/java" ] ; then
|
||||
JAVA_HOME=$APP_HOME/.jdk/current
|
||||
export JAVA_HOME
|
||||
fi
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,93 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,12 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = "Fabric"
|
||||
url = "https://maven.fabricmc.net/"
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "computer"
|
||||
@@ -0,0 +1,46 @@
|
||||
package fr.koka99cab.computer.client;
|
||||
|
||||
import fr.koka99cab.computer.client.gui.ComputerDesktopScreen;
|
||||
import fr.koka99cab.computer.client.voxel.ComputerBuildPlacementController;
|
||||
import fr.koka99cab.computer.network.ComputerOpenPayload;
|
||||
import fr.koka99cab.computer.network.ComputerStatsPayload;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.event.player.UseItemCallback;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
|
||||
public final class ComputerClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ComputerLiveStats.register();
|
||||
ComputerBuildPlacementController.initialize();
|
||||
ClientPlayNetworking.registerGlobalReceiver(ComputerOpenPayload.TYPE, (payload, context) ->
|
||||
context.client().execute(() -> context.client().setScreen(new ComputerDesktopScreen())));
|
||||
ClientPlayNetworking.registerGlobalReceiver(ComputerStatsPayload.TYPE, (payload, context) ->
|
||||
context.client().execute(() -> ComputerDesktopScreen.acceptStatsPayload(payload)));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ComputerBuildPlacementController.reset());
|
||||
AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> {
|
||||
if (world.isClientSide()) {
|
||||
return ComputerBuildPlacementController.confirmFromAttack(pos, direction);
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
});
|
||||
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> {
|
||||
if (world.isClientSide()) {
|
||||
return ComputerBuildPlacementController.confirmFromUse(hitResult);
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
});
|
||||
UseItemCallback.EVENT.register((player, world, hand) -> {
|
||||
if (world.isClientSide()) {
|
||||
return ComputerBuildPlacementController.confirmCurrentAnchor();
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(ComputerBuildPlacementController::onEndTick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package fr.koka99cab.computer.client;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.stats.StatsCounter;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class ComputerLiveStats {
|
||||
private static final double MAX_TRACKED_TICK_DISTANCE = 24.0D;
|
||||
private static final long HISTORY_SAMPLE_INTERVAL_MS = 2000L;
|
||||
private static String sessionKey = "";
|
||||
private static Vec3 lastPosition;
|
||||
private static long livePlayTimeTicks;
|
||||
private static long liveDistanceCm;
|
||||
private static long liveMinedBlocks;
|
||||
private static long livePlacedBlocks;
|
||||
private static long livePickedBlocks;
|
||||
private static long baseCraftedItems = -1L;
|
||||
private static long baseMobKills = -1L;
|
||||
private static long basePlayTimeTicks = -1L;
|
||||
private static long baseDistanceCm = -1L;
|
||||
private static long baseMinedBlocks = -1L;
|
||||
private static long basePlacedBlocks = -1L;
|
||||
private static long basePickedBlocks = -1L;
|
||||
private static long lastHistorySampleMs;
|
||||
private static Map<String, Long> basePickedFamilies = Map.of();
|
||||
private static Map<String, Long> livePickedFamilies = new HashMap<>();
|
||||
private static Map<String, Long> lastInventoryFamilies = Map.of();
|
||||
private static final List<LiveSample> historySamples = new ArrayList<>();
|
||||
|
||||
private ComputerLiveStats() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ClientTickEvents.END_CLIENT_TICK.register(ComputerLiveStats::tick);
|
||||
ClientPlayerBlockBreakEvents.AFTER.register((level, player, pos, state) -> {
|
||||
ensureSession(Minecraft.getInstance());
|
||||
if (!state.isAir()) {
|
||||
liveMinedBlocks++;
|
||||
}
|
||||
});
|
||||
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> {
|
||||
if (world.isClientSide() && hand == InteractionHand.MAIN_HAND && player == Minecraft.getInstance().player) {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (!stack.isEmpty() && stack.getItem() instanceof BlockItem) {
|
||||
ensureSession(Minecraft.getInstance());
|
||||
livePlacedBlocks++;
|
||||
}
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
});
|
||||
}
|
||||
|
||||
private static void tick(Minecraft client) {
|
||||
if (client.player == null || client.level == null) {
|
||||
lastPosition = null;
|
||||
lastInventoryFamilies = Map.of();
|
||||
return;
|
||||
}
|
||||
ensureSession(client);
|
||||
livePlayTimeTicks++;
|
||||
Vec3 position = client.player.position();
|
||||
if (lastPosition != null) {
|
||||
double distance = position.distanceTo(lastPosition);
|
||||
if (distance > 0.0D && distance <= MAX_TRACKED_TICK_DISTANCE) {
|
||||
liveDistanceCm += Math.round(distance * 100.0D);
|
||||
}
|
||||
}
|
||||
lastPosition = position;
|
||||
updateInventoryPickups(client.player);
|
||||
recordHistorySample(client, false);
|
||||
}
|
||||
|
||||
private static void ensureSession(Minecraft client) {
|
||||
String key = sessionKey(client);
|
||||
if (key.equals(sessionKey)) {
|
||||
return;
|
||||
}
|
||||
sessionKey = key;
|
||||
lastPosition = null;
|
||||
livePlayTimeTicks = 0L;
|
||||
liveDistanceCm = 0L;
|
||||
liveMinedBlocks = 0L;
|
||||
livePlacedBlocks = 0L;
|
||||
livePickedBlocks = 0L;
|
||||
baseCraftedItems = -1L;
|
||||
baseMobKills = -1L;
|
||||
lastHistorySampleMs = 0L;
|
||||
livePickedFamilies = new HashMap<>();
|
||||
lastInventoryFamilies = Map.of();
|
||||
historySamples.clear();
|
||||
StatsCounter stats = client.player == null ? null : client.player.getStats();
|
||||
if (stats == null) {
|
||||
basePlayTimeTicks = -1L;
|
||||
baseDistanceCm = -1L;
|
||||
baseMinedBlocks = -1L;
|
||||
basePlacedBlocks = -1L;
|
||||
basePickedBlocks = -1L;
|
||||
baseCraftedItems = -1L;
|
||||
baseMobKills = -1L;
|
||||
basePickedFamilies = Map.of();
|
||||
} else {
|
||||
basePlayTimeTicks = customStat(stats, Stats.PLAY_TIME);
|
||||
baseDistanceCm = distanceCentimeters(stats);
|
||||
baseMinedBlocks = minedBlocks(stats);
|
||||
basePlacedBlocks = usedBlockItems(stats);
|
||||
basePickedBlocks = pickedBlockItems(stats);
|
||||
baseCraftedItems = craftedItems(stats);
|
||||
baseMobKills = customStat(stats, Stats.MOB_KILLS);
|
||||
basePickedFamilies = pickedBlockFamilies(stats);
|
||||
recordHistorySample(client, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static String sessionKey(Minecraft client) {
|
||||
if (client.player == null || client.level == null) {
|
||||
return "";
|
||||
}
|
||||
return client.player.getUUID() + "@" + client.level.dimension().identifier();
|
||||
}
|
||||
|
||||
private static void updateInventoryPickups(LocalPlayer player) {
|
||||
Map<String, Long> current = inventoryBlockFamilies(player);
|
||||
if (!lastInventoryFamilies.isEmpty()) {
|
||||
for (Map.Entry<String, Long> entry : current.entrySet()) {
|
||||
long previous = lastInventoryFamilies.getOrDefault(entry.getKey(), 0L);
|
||||
long delta = entry.getValue() - previous;
|
||||
if (delta > 0L) {
|
||||
livePickedBlocks += delta;
|
||||
livePickedFamilies.merge(entry.getKey(), delta, Long::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastInventoryFamilies = current;
|
||||
}
|
||||
|
||||
private static Map<String, Long> inventoryBlockFamilies(LocalPlayer player) {
|
||||
Map<String, Long> counts = new LinkedHashMap<>();
|
||||
Inventory inventory = player.getInventory();
|
||||
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
|
||||
ItemStack stack = inventory.getItem(slot);
|
||||
if (!stack.isEmpty() && stack.getItem() instanceof BlockItem blockItem) {
|
||||
counts.merge(blockFamily(blockItem.getBlock()), (long) stack.getCount(), Long::sum);
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
public static long playTimeTicks(long vanillaValue) {
|
||||
if (basePlayTimeTicks < 0L) {
|
||||
basePlayTimeTicks = vanillaValue;
|
||||
}
|
||||
return Math.max(vanillaValue, basePlayTimeTicks + livePlayTimeTicks);
|
||||
}
|
||||
|
||||
public static long distanceCm(long vanillaValue) {
|
||||
if (baseDistanceCm < 0L) {
|
||||
baseDistanceCm = vanillaValue;
|
||||
}
|
||||
return Math.max(vanillaValue, baseDistanceCm + liveDistanceCm);
|
||||
}
|
||||
|
||||
public static long minedBlocks(long vanillaValue) {
|
||||
if (baseMinedBlocks < 0L) {
|
||||
baseMinedBlocks = vanillaValue;
|
||||
}
|
||||
return Math.max(vanillaValue, baseMinedBlocks + liveMinedBlocks);
|
||||
}
|
||||
|
||||
public static long placedBlocks(long vanillaValue) {
|
||||
if (basePlacedBlocks < 0L) {
|
||||
basePlacedBlocks = vanillaValue;
|
||||
}
|
||||
return Math.max(vanillaValue, basePlacedBlocks + livePlacedBlocks);
|
||||
}
|
||||
|
||||
public static long pickedBlocks(long vanillaValue) {
|
||||
if (basePickedBlocks < 0L) {
|
||||
basePickedBlocks = vanillaValue;
|
||||
}
|
||||
return Math.max(vanillaValue, basePickedBlocks + livePickedBlocks);
|
||||
}
|
||||
|
||||
public static Map<String, Long> pickedBlockFamilies(Map<String, Long> vanillaFamilies) {
|
||||
if (basePickedFamilies.isEmpty() && !vanillaFamilies.isEmpty()) {
|
||||
basePickedFamilies = new HashMap<>(vanillaFamilies);
|
||||
}
|
||||
Map<String, Long> merged = new LinkedHashMap<>(vanillaFamilies);
|
||||
for (Map.Entry<String, Long> entry : livePickedFamilies.entrySet()) {
|
||||
long base = basePickedFamilies.getOrDefault(entry.getKey(), 0L);
|
||||
long liveValue = base + entry.getValue();
|
||||
merged.merge(entry.getKey(), liveValue, Math::max);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
public static List<LiveSample> historySamples() {
|
||||
return List.copyOf(historySamples);
|
||||
}
|
||||
|
||||
private static void recordHistorySample(Minecraft client, boolean force) {
|
||||
if (client.player == null) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (!force && now - lastHistorySampleMs < HISTORY_SAMPLE_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
StatsCounter stats = client.player.getStats();
|
||||
LiveSample sample = new LiveSample(
|
||||
playTimeTicks(customStat(stats, Stats.PLAY_TIME)),
|
||||
distanceCm(distanceCentimeters(stats)),
|
||||
sessionValue(baseMinedBlocks, liveMinedBlocks),
|
||||
Math.max(baseCraftedItems, craftedItems(stats)),
|
||||
sessionValue(basePickedBlocks, livePickedBlocks),
|
||||
Math.max(baseMobKills, customStat(stats, Stats.MOB_KILLS))
|
||||
);
|
||||
if (historySamples.isEmpty() || !historySamples.getLast().equals(sample)) {
|
||||
historySamples.add(sample);
|
||||
}
|
||||
lastHistorySampleMs = now;
|
||||
}
|
||||
|
||||
private static long sessionValue(long base, long live) {
|
||||
return Math.max(0L, (base < 0L ? 0L : base) + live);
|
||||
}
|
||||
|
||||
private static long customStat(StatsCounter stats, Identifier id) {
|
||||
return stats.getValue(Stats.CUSTOM.get(id));
|
||||
}
|
||||
|
||||
private static long distanceCentimeters(StatsCounter stats) {
|
||||
return customStat(stats, Stats.WALK_ONE_CM)
|
||||
+ customStat(stats, Stats.CROUCH_ONE_CM)
|
||||
+ customStat(stats, Stats.SPRINT_ONE_CM)
|
||||
+ customStat(stats, Stats.WALK_ON_WATER_ONE_CM)
|
||||
+ customStat(stats, Stats.WALK_UNDER_WATER_ONE_CM)
|
||||
+ customStat(stats, Stats.SWIM_ONE_CM)
|
||||
+ customStat(stats, Stats.FLY_ONE_CM)
|
||||
+ customStat(stats, Stats.AVIATE_ONE_CM)
|
||||
+ customStat(stats, Stats.BOAT_ONE_CM)
|
||||
+ customStat(stats, Stats.MINECART_ONE_CM)
|
||||
+ customStat(stats, Stats.HORSE_ONE_CM);
|
||||
}
|
||||
|
||||
private static long minedBlocks(StatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
total += stats.getValue(Stats.BLOCK_MINED, block);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long pickedBlockItems(StatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item instanceof BlockItem) {
|
||||
total += stats.getValue(Stats.ITEM_PICKED_UP, item);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long craftedItems(StatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
total += stats.getValue(Stats.ITEM_CRAFTED, item);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long usedBlockItems(StatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item instanceof BlockItem) {
|
||||
total += stats.getValue(Stats.ITEM_USED, item);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static Map<String, Long> pickedBlockFamilies(StatsCounter stats) {
|
||||
Map<String, Long> counts = new LinkedHashMap<>();
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item instanceof BlockItem blockItem) {
|
||||
long count = stats.getValue(Stats.ITEM_PICKED_UP, item);
|
||||
if (count > 0L) {
|
||||
counts.merge(blockFamily(blockItem.getBlock()), count, Long::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
private static String blockFamily(Block block) {
|
||||
Identifier id = BuiltInRegistries.BLOCK.getKey(block);
|
||||
String path = id == null ? "" : id.getPath().toLowerCase(Locale.ROOT);
|
||||
if (path.contains("ore") || path.contains("raw_") || path.contains("coal") || path.contains("copper") || path.contains("iron") || path.contains("gold") || path.contains("diamond") || path.contains("emerald") || path.contains("lapis") || path.contains("redstone") || path.contains("quartz") || path.contains("ancient_debris")) {
|
||||
return "Minerais";
|
||||
}
|
||||
if (path.contains("log") || path.contains("wood") || path.contains("planks") || path.contains("stem") || path.contains("hyphae") || path.contains("bamboo")) {
|
||||
return "Bois";
|
||||
}
|
||||
if (path.contains("stone") || path.contains("deepslate") || path.contains("granite") || path.contains("andesite") || path.contains("diorite") || path.contains("tuff") || path.contains("basalt") || path.contains("blackstone") || path.contains("netherrack")) {
|
||||
return "Roche";
|
||||
}
|
||||
if (path.contains("dirt") || path.contains("grass_block") || path.contains("sand") || path.contains("gravel") || path.contains("clay") || path.contains("mud") || path.contains("soul")) {
|
||||
return "Sols";
|
||||
}
|
||||
if (path.contains("leaves") || path.contains("sapling") || path.contains("flower") || path.contains("moss") || path.contains("grass") || path.contains("vine") || path.contains("roots") || path.contains("crop")) {
|
||||
return "Vegetal";
|
||||
}
|
||||
if (path.contains("glass") || path.contains("wool") || path.contains("concrete") || path.contains("terracotta") || path.contains("brick")) {
|
||||
return "Construits";
|
||||
}
|
||||
return "Autres";
|
||||
}
|
||||
|
||||
public record LiveSample(long playTimeTicks, long distanceCm, long minedBlocks, long craftedItems, long pickedBlocks, long mobKills) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package fr.koka99cab.computer.client.gui;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
final class BlocotechDescriptions {
|
||||
private static final String RESOURCE_PATH = "/assets/computer/data/blocotech_descriptions.json";
|
||||
private static volatile BlocotechDescriptions defaultDescriptions;
|
||||
|
||||
private final String minecraftVersion;
|
||||
private final Map<String, Entry> entries;
|
||||
|
||||
private BlocotechDescriptions(String minecraftVersion, Map<String, Entry> entries) {
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
this.entries = Map.copyOf(entries);
|
||||
}
|
||||
|
||||
static BlocotechDescriptions loadDefault() {
|
||||
BlocotechDescriptions loaded = defaultDescriptions;
|
||||
if (loaded != null) {
|
||||
return loaded;
|
||||
}
|
||||
synchronized (BlocotechDescriptions.class) {
|
||||
if (defaultDescriptions != null) {
|
||||
return defaultDescriptions;
|
||||
}
|
||||
try (InputStream stream = BlocotechDescriptions.class.getResourceAsStream(RESOURCE_PATH)) {
|
||||
if (stream == null) {
|
||||
ComputerMod.LOGGER.warn("Blocotech descriptions not found: {}", RESOURCE_PATH);
|
||||
defaultDescriptions = empty();
|
||||
return defaultDescriptions;
|
||||
}
|
||||
try (Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
|
||||
defaultDescriptions = fromReader(reader);
|
||||
ComputerMod.LOGGER.info("Loaded {} Blocotech descriptions for Minecraft {}.", defaultDescriptions.size(), defaultDescriptions.minecraftVersion());
|
||||
return defaultDescriptions;
|
||||
}
|
||||
} catch (IOException | IllegalStateException exception) {
|
||||
ComputerMod.LOGGER.warn("Could not load Blocotech descriptions.", exception);
|
||||
defaultDescriptions = empty();
|
||||
return defaultDescriptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BlocotechDescriptions fromReader(Reader reader) {
|
||||
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Map<String, Entry> entries = new LinkedHashMap<>();
|
||||
for (JsonElement element : array(root, "entries")) {
|
||||
if (!element.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
Entry entry = entry(element.getAsJsonObject());
|
||||
if (!entry.id().isBlank()) {
|
||||
entries.put(entry.id(), entry);
|
||||
}
|
||||
}
|
||||
return new BlocotechDescriptions(string(root, "minecraft_version", ""), entries);
|
||||
}
|
||||
|
||||
private static BlocotechDescriptions empty() {
|
||||
return new BlocotechDescriptions("", Map.of());
|
||||
}
|
||||
|
||||
Optional<Entry> entry(String id) {
|
||||
return Optional.ofNullable(entries.get(id));
|
||||
}
|
||||
|
||||
int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
String minecraftVersion() {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
private static Entry entry(JsonObject object) {
|
||||
return new Entry(
|
||||
string(object, "id", ""),
|
||||
string(object, "name", ""),
|
||||
string(object, "description", ""),
|
||||
string(object, "category", ""),
|
||||
string(object, "family", ""),
|
||||
string(object, "primary", "#000000"),
|
||||
string(object, "hue_group", "")
|
||||
);
|
||||
}
|
||||
|
||||
private static JsonArray array(JsonObject object, String key) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonArray() ? element.getAsJsonArray() : new JsonArray();
|
||||
}
|
||||
|
||||
private static String string(JsonObject object, String key, String fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonPrimitive() ? element.getAsString() : fallback;
|
||||
}
|
||||
|
||||
record Entry(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String category,
|
||||
String family,
|
||||
String primary,
|
||||
String hueGroup
|
||||
) {
|
||||
String displayName() {
|
||||
return name.isBlank() ? id : name;
|
||||
}
|
||||
|
||||
int primaryArgb() {
|
||||
return hexToArgb(primary);
|
||||
}
|
||||
}
|
||||
|
||||
private static int hexToArgb(String hex) {
|
||||
if (hex == null || !hex.matches("#?[0-9a-fA-F]{6}")) {
|
||||
return 0xFF6B7483;
|
||||
}
|
||||
String cleaned = hex.startsWith("#") ? hex.substring(1) : hex;
|
||||
return 0xFF000000 | Integer.parseInt(cleaned, 16);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.koka99cab.computer.client.gui;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.renderer.state.gui.GuiElementRenderState;
|
||||
import net.minecraft.client.renderer.state.gui.GuiRenderState;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
final class StudioGuiRenderAccess {
|
||||
private static final Field GUI_RENDER_STATE_FIELD = guiRenderStateField();
|
||||
|
||||
private StudioGuiRenderAccess() {
|
||||
}
|
||||
|
||||
static boolean submit(GuiGraphicsExtractor graphics, GuiElementRenderState state) {
|
||||
if (GUI_RENDER_STATE_FIELD == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
((GuiRenderState) GUI_RENDER_STATE_FIELD.get(graphics)).addGuiElement(state);
|
||||
return true;
|
||||
} catch (IllegalAccessException | ClassCastException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Field guiRenderStateField() {
|
||||
try {
|
||||
Field field = GuiGraphicsExtractor.class.getDeclaredField("guiRenderState");
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (NoSuchFieldException | SecurityException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package fr.koka99cab.computer.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||
import net.minecraft.client.gui.render.TextureSetup;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.state.gui.GuiElementRenderState;
|
||||
import org.joml.Matrix3x2f;
|
||||
import org.joml.Matrix3x2fc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class StudioViewportRenderState implements GuiElementRenderState {
|
||||
private static final RenderPipeline DEFAULT_PIPELINE = RenderPipelines.GUI;
|
||||
private static final TextureSetup DEFAULT_TEXTURE_SETUP = TextureSetup.noTexture();
|
||||
|
||||
private final Matrix3x2f pose;
|
||||
private final List<Quad> quads;
|
||||
private final ScreenRectangle scissorArea;
|
||||
private final ScreenRectangle bounds;
|
||||
private final RenderPipeline pipeline;
|
||||
private final TextureSetup textureSetup;
|
||||
private final boolean textured;
|
||||
|
||||
StudioViewportRenderState(Matrix3x2fc pose, List<Quad> quads, ScreenRectangle scissorArea, RenderPipeline pipeline, TextureSetup textureSetup, boolean textured) {
|
||||
this.pose = new Matrix3x2f(pose);
|
||||
this.quads = List.copyOf(quads);
|
||||
this.scissorArea = scissorArea;
|
||||
this.bounds = bounds(quads, scissorArea);
|
||||
this.pipeline = pipeline;
|
||||
this.textureSetup = textureSetup;
|
||||
this.textured = textured;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildVertices(VertexConsumer consumer) {
|
||||
for (Quad quad : quads) {
|
||||
addVertex(consumer, quad.x0(), quad.y0(), quad.u0(), quad.v0(), quad.color());
|
||||
addVertex(consumer, quad.x1(), quad.y1(), quad.u1(), quad.v1(), quad.color());
|
||||
addVertex(consumer, quad.x2(), quad.y2(), quad.u2(), quad.v2(), quad.color());
|
||||
addVertex(consumer, quad.x3(), quad.y3(), quad.u3(), quad.v3(), quad.color());
|
||||
}
|
||||
}
|
||||
|
||||
private void addVertex(VertexConsumer consumer, float x, float y, float u, float v, int color) {
|
||||
VertexConsumer vertex = consumer.addVertexWith2DPose(pose, x, y);
|
||||
if (textured) {
|
||||
vertex.setUv(u, v);
|
||||
}
|
||||
vertex.setColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderPipeline pipeline() {
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextureSetup textureSetup() {
|
||||
return textureSetup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScreenRectangle scissorArea() {
|
||||
return scissorArea;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScreenRectangle bounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
static Builder builder() {
|
||||
return new Builder(DEFAULT_PIPELINE, DEFAULT_TEXTURE_SETUP, false);
|
||||
}
|
||||
|
||||
static Builder texturedBuilder(TextureSetup textureSetup) {
|
||||
return new Builder(RenderPipelines.GUI_TEXTURED, textureSetup, true);
|
||||
}
|
||||
|
||||
private static ScreenRectangle bounds(List<Quad> quads, ScreenRectangle fallback) {
|
||||
if (quads.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
int left = Integer.MAX_VALUE;
|
||||
int top = Integer.MAX_VALUE;
|
||||
int right = Integer.MIN_VALUE;
|
||||
int bottom = Integer.MIN_VALUE;
|
||||
for (Quad quad : quads) {
|
||||
left = Math.min(left, (int) Math.floor(Math.min(Math.min(quad.x0(), quad.x1()), Math.min(quad.x2(), quad.x3()))));
|
||||
top = Math.min(top, (int) Math.floor(Math.min(Math.min(quad.y0(), quad.y1()), Math.min(quad.y2(), quad.y3()))));
|
||||
right = Math.max(right, (int) Math.ceil(Math.max(Math.max(quad.x0(), quad.x1()), Math.max(quad.x2(), quad.x3()))));
|
||||
bottom = Math.max(bottom, (int) Math.ceil(Math.max(Math.max(quad.y0(), quad.y1()), Math.max(quad.y2(), quad.y3()))));
|
||||
}
|
||||
return new ScreenRectangle(left, top, Math.max(1, right - left), Math.max(1, bottom - top));
|
||||
}
|
||||
|
||||
static final class Builder {
|
||||
private final List<Quad> quads = new ArrayList<>();
|
||||
private final RenderPipeline pipeline;
|
||||
private final TextureSetup textureSetup;
|
||||
private final boolean textured;
|
||||
|
||||
private Builder(RenderPipeline pipeline, TextureSetup textureSetup, boolean textured) {
|
||||
this.pipeline = pipeline;
|
||||
this.textureSetup = textureSetup;
|
||||
this.textured = textured;
|
||||
}
|
||||
|
||||
void rect(int x0, int y0, int x1, int y1, int color) {
|
||||
if (x0 == x1 || y0 == y1 || (color >>> 24) == 0) {
|
||||
return;
|
||||
}
|
||||
int left = Math.min(x0, x1);
|
||||
int right = Math.max(x0, x1);
|
||||
int top = Math.min(y0, y1);
|
||||
int bottom = Math.max(y0, y1);
|
||||
float rightEdge = Math.max(left + 1, right);
|
||||
float bottomEdge = Math.max(top + 1, bottom);
|
||||
quads.add(new Quad(left, top, 0.0F, 0.0F, left, bottomEdge, 0.0F, 0.0F, rightEdge, bottomEdge, 0.0F, 0.0F, rightEdge, top, 0.0F, 0.0F, color));
|
||||
}
|
||||
|
||||
void line(int x0, int y0, int x1, int y1, int thickness, int color) {
|
||||
if ((color >>> 24) == 0) {
|
||||
return;
|
||||
}
|
||||
float dx = x1 - x0;
|
||||
float dy = y1 - y0;
|
||||
float length = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
if (length <= 0.001F) {
|
||||
rect(x0, y0, x0 + Math.max(1, thickness), y0 + Math.max(1, thickness), color);
|
||||
return;
|
||||
}
|
||||
float half = Math.max(1.0F, thickness) * 0.5F;
|
||||
float nx = -dy / length * half;
|
||||
float ny = dx / length * half;
|
||||
quads.add(new Quad(x0 + nx, y0 + ny, 0.0F, 0.0F, x1 + nx, y1 + ny, 0.0F, 0.0F, x1 - nx, y1 - ny, 0.0F, 0.0F, x0 - nx, y0 - ny, 0.0F, 0.0F, color));
|
||||
}
|
||||
|
||||
void quad(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, int color) {
|
||||
quad(x0, y0, 0.0F, 0.0F, x1, y1, 0.0F, 0.0F, x2, y2, 0.0F, 0.0F, x3, y3, 0.0F, 0.0F, color);
|
||||
}
|
||||
|
||||
void texturedQuad(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float u0, float v0, float u1, float v1, int color) {
|
||||
quad(x0, y0, u0, v0, x1, y1, u1, v0, x2, y2, u1, v1, x3, y3, u0, v1, color);
|
||||
}
|
||||
|
||||
private void quad(float x0, float y0, float u0, float v0, float x1, float y1, float u1, float v1, float x2, float y2, float u2, float v2, float x3, float y3, float u3, float v3, int color) {
|
||||
if ((color >>> 24) == 0) {
|
||||
return;
|
||||
}
|
||||
if (signedArea(x0, y0, x1, y1, x2, y2, x3, y3) > 0.0F) {
|
||||
quads.add(new Quad(x0, y0, u0, v0, x3, y3, u3, v3, x2, y2, u2, v2, x1, y1, u1, v1, color));
|
||||
} else {
|
||||
quads.add(new Quad(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2, x3, y3, u3, v3, color));
|
||||
}
|
||||
}
|
||||
|
||||
private float signedArea(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3) {
|
||||
return 0.5F * (
|
||||
x0 * y1 - y0 * x1
|
||||
+ x1 * y2 - y1 * x2
|
||||
+ x2 * y3 - y2 * x3
|
||||
+ x3 * y0 - y3 * x0
|
||||
);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return quads.isEmpty();
|
||||
}
|
||||
|
||||
StudioViewportRenderState build(Matrix3x2fc pose, ScreenRectangle scissorArea) {
|
||||
return new StudioViewportRenderState(pose, quads, scissorArea, pipeline, textureSetup, textured);
|
||||
}
|
||||
}
|
||||
|
||||
private record Quad(float x0, float y0, float u0, float v0, float x1, float y1, float u1, float v1, float x2, float y2, float u2, float v2, float x3, float y3, float u3, float v3, int color) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package fr.koka99cab.computer.client.voxel;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka99cab.computer.network.ComputerPlaceVoxelProjectPayload;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelPlacement;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelProject;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gizmos.GizmoStyle;
|
||||
import net.minecraft.gizmos.Gizmos;
|
||||
import net.minecraft.gizmos.TextGizmo;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.ClipContext;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
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 ComputerBuildPlacementController {
|
||||
private static final double RAYCAST_DISTANCE = 128.0D;
|
||||
private static final int BLUEPRINT_COLOR = 0xFF62D6A4;
|
||||
private static final int DIRECT_COLOR = 0xFFFFC857;
|
||||
private static final int CURRENT_COLOR = 0xFFB7F7FF;
|
||||
private static final int MAX_PREVIEW_CELLS = 4096;
|
||||
private static final Map<String, Integer> BLOCK_COLOR_CACHE = new HashMap<>();
|
||||
private static KeyMapping rotate;
|
||||
private static KeyMapping cancel;
|
||||
private static KeyMapping confirm;
|
||||
private static KeyMapping undo;
|
||||
private static KeyMapping redo;
|
||||
private static ComputerVoxelProject activeProject;
|
||||
private static Mode mode = Mode.BLUEPRINT;
|
||||
private static BlockPos anchor;
|
||||
private static Direction facing = Direction.NORTH;
|
||||
private static boolean awaitingAnchor;
|
||||
private static boolean previewVisible = true;
|
||||
|
||||
private ComputerBuildPlacementController() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (rotate != null && cancel != null && confirm != null) {
|
||||
return;
|
||||
}
|
||||
rotate = KeyMappingHelper.registerKeyMapping(new KeyMapping("key.computer.rotate_blueprint", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_R, KeyMapping.Category.INVENTORY));
|
||||
cancel = KeyMappingHelper.registerKeyMapping(new KeyMapping("key.computer.cancel_blueprint", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_V, KeyMapping.Category.INVENTORY));
|
||||
confirm = KeyMappingHelper.registerKeyMapping(new KeyMapping("key.computer.confirm_blueprint", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_ENTER, KeyMapping.Category.INVENTORY));
|
||||
undo = KeyMappingHelper.registerKeyMapping(new KeyMapping("key.computer.undo_construction", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_Z, KeyMapping.Category.INVENTORY));
|
||||
redo = KeyMappingHelper.registerKeyMapping(new KeyMapping("key.computer.redo_construction", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_Y, KeyMapping.Category.INVENTORY));
|
||||
}
|
||||
|
||||
public static void startBlueprint(ComputerVoxelProject project) {
|
||||
start(project, Mode.BLUEPRINT);
|
||||
}
|
||||
|
||||
public static void startCreativeDirect(ComputerVoxelProject project) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (client.player != null && !client.player.isCreative()) {
|
||||
client.player.sendOverlayMessage(Component.literal("Pose directe reservee au mode creatif."));
|
||||
return;
|
||||
}
|
||||
start(project, Mode.CREATIVE_DIRECT);
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
activeProject = null;
|
||||
anchor = null;
|
||||
awaitingAnchor = false;
|
||||
mode = Mode.BLUEPRINT;
|
||||
previewVisible = true;
|
||||
}
|
||||
|
||||
public static boolean isConstructionActive() {
|
||||
return activeProject != null;
|
||||
}
|
||||
|
||||
public static void onEndTick(Minecraft client) {
|
||||
if (client.player == null || client.level == null) {
|
||||
if (activeProject != null) {
|
||||
reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (client.screen == null) {
|
||||
while (undo != null && undo.consumeClick()) {
|
||||
requestHistory(ComputerPlaceVoxelProjectPayload.MODE_UNDO);
|
||||
}
|
||||
while (redo != null && redo.consumeClick()) {
|
||||
requestHistory(ComputerPlaceVoxelProjectPayload.MODE_REDO);
|
||||
}
|
||||
}
|
||||
if (activeProject == null) {
|
||||
return;
|
||||
}
|
||||
if (awaitingAnchor) {
|
||||
anchor = resolvePreviewAnchor(client);
|
||||
}
|
||||
if (client.screen == null) {
|
||||
while (rotate != null && rotate.consumeClick()) {
|
||||
if (awaitingAnchor) {
|
||||
facing = facing.getClockWise();
|
||||
}
|
||||
}
|
||||
while (cancel != null && cancel.consumeClick()) {
|
||||
reset();
|
||||
client.player.sendOverlayMessage(Component.literal("Blueprint annule."));
|
||||
}
|
||||
while (awaitingAnchor && confirm != null && confirm.consumeClick()) {
|
||||
if (anchor != null) {
|
||||
confirmAnchor(anchor);
|
||||
}
|
||||
}
|
||||
while (awaitingAnchor && client.options.keyUse.consumeClick()) {
|
||||
if (anchor != null) {
|
||||
confirmAnchor(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!awaitingAnchor && mode == Mode.BLUEPRINT && anchor != null && activeProject != null) {
|
||||
Progress progress = progress(client, activeProject, anchor, facing);
|
||||
if (progress.total() > 0 && progress.done() >= progress.total()) {
|
||||
reset();
|
||||
client.player.sendOverlayMessage(Component.literal("Blueprint termine."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (previewVisible) {
|
||||
render(client);
|
||||
}
|
||||
}
|
||||
|
||||
public static InteractionResult confirmFromAttack(BlockPos pos, Direction face) {
|
||||
if (face == null) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
return confirmAnchor(pos.relative(face));
|
||||
}
|
||||
|
||||
public static InteractionResult confirmFromUse(BlockHitResult hitResult) {
|
||||
return confirmAnchor(hitResult.getBlockPos().relative(hitResult.getDirection()));
|
||||
}
|
||||
|
||||
public static InteractionResult confirmCurrentAnchor() {
|
||||
if (anchor == null) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
return confirmAnchor(anchor);
|
||||
}
|
||||
|
||||
private static void start(ComputerVoxelProject project, Mode nextMode) {
|
||||
activeProject = project;
|
||||
mode = nextMode;
|
||||
awaitingAnchor = true;
|
||||
anchor = null;
|
||||
previewVisible = true;
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (client.player != null) {
|
||||
facing = horizontal(client.player.getDirection());
|
||||
client.player.sendOverlayMessage(Component.literal(nextMode == Mode.CREATIVE_DIRECT ? "Choisis l'ancre de pose directe." : "Choisis l'ancre du blueprint."));
|
||||
}
|
||||
}
|
||||
|
||||
private static InteractionResult confirmAnchor(BlockPos selectedAnchor) {
|
||||
if (activeProject == null || !awaitingAnchor) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
anchor = selectedAnchor.immutable();
|
||||
awaitingAnchor = false;
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (mode == Mode.CREATIVE_DIRECT) {
|
||||
if (ClientPlayNetworking.canSend(ComputerPlaceVoxelProjectPayload.TYPE)) {
|
||||
ClientPlayNetworking.send(new ComputerPlaceVoxelProjectPayload(activeProject, anchor, facing, ComputerPlaceVoxelProjectPayload.MODE_CREATIVE_DIRECT));
|
||||
}
|
||||
reset();
|
||||
} else if (client.player != null) {
|
||||
client.player.sendOverlayMessage(Component.literal("Blueprint actif: construis le bloc indique."));
|
||||
}
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
|
||||
private static void requestHistory(String action) {
|
||||
if (ClientPlayNetworking.canSend(ComputerPlaceVoxelProjectPayload.TYPE)) {
|
||||
ClientPlayNetworking.send(new ComputerPlaceVoxelProjectPayload(ComputerVoxelProject.empty(), BlockPos.ZERO, Direction.NORTH, action));
|
||||
}
|
||||
}
|
||||
|
||||
private static void render(Minecraft client) {
|
||||
if (anchor == null || activeProject == null) {
|
||||
return;
|
||||
}
|
||||
try (Gizmos.TemporaryCollection ignored = client.collectPerTickGizmos()) {
|
||||
AABB box = projectBox(activeProject, anchor, facing);
|
||||
int color = mode == Mode.CREATIVE_DIRECT ? DIRECT_COLOR : BLUEPRINT_COLOR;
|
||||
Gizmos.cuboid(box, GizmoStyle.strokeAndFill(color, 2.0F, withAlpha(color, 0x18)), false).setAlwaysOnTop();
|
||||
renderProjectCells(client, activeProject, anchor, facing);
|
||||
Vec3 center = box.getCenter();
|
||||
String title = mode == Mode.CREATIVE_DIRECT ? "Pose creative: " + activeProject.name() : activeProject.name();
|
||||
Gizmos.billboardText(title, new Vec3(center.x, box.maxY + 1.25D, center.z), TextGizmo.Style.forColorAndCentered(color).withScale(0.78F)).setAlwaysOnTop();
|
||||
if (awaitingAnchor) {
|
||||
Gizmos.billboardText("Clic droit/Entree ancrer | R tourner | V annuler", new Vec3(center.x, box.maxY + 0.72D, center.z), TextGizmo.Style.forColorAndCentered(0xFFD3ECFF).withScale(0.58F)).setAlwaysOnTop();
|
||||
return;
|
||||
}
|
||||
Progress progress = progress(client, activeProject, anchor, facing);
|
||||
Gizmos.billboardText(progress.done() + " / " + progress.total(), new Vec3(center.x, box.maxY + 0.72D, center.z), TextGizmo.Style.forColorAndCentered(0xFFD3ECFF).withScale(0.62F)).setAlwaysOnTop();
|
||||
if (progress.current() != null) {
|
||||
BlockPos pos = ComputerVoxelPlacement.transform(activeProject, progress.current(), anchor, facing);
|
||||
Gizmos.cuboid(new AABB(pos), GizmoStyle.strokeAndFill(CURRENT_COLOR, 2.8F, withAlpha(CURRENT_COLOR, 0x35)), false).setAlwaysOnTop();
|
||||
Gizmos.billboardText(shortId(progress.current().blockId()), new Vec3(pos.getX() + 0.5D, pos.getY() + 1.25D, pos.getZ() + 0.5D), TextGizmo.Style.forColorAndCentered(CURRENT_COLOR).withScale(0.55F)).setAlwaysOnTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Progress progress(Minecraft client, ComputerVoxelProject project, BlockPos anchor, Direction facing) {
|
||||
List<ComputerVoxelPlacement.Cell> cells = ComputerVoxelPlacement.orderedCells(project);
|
||||
int done = 0;
|
||||
ComputerVoxelPlacement.Cell current = null;
|
||||
for (ComputerVoxelPlacement.Cell cell : cells) {
|
||||
BlockPos worldPos = ComputerVoxelPlacement.transform(project, cell, anchor, facing);
|
||||
if (matches(client, worldPos, cell.blockId())) {
|
||||
done++;
|
||||
} else if (current == null) {
|
||||
current = cell;
|
||||
}
|
||||
}
|
||||
return new Progress(done, cells.size(), current);
|
||||
}
|
||||
|
||||
private static boolean matches(Minecraft client, BlockPos pos, String blockId) {
|
||||
if (client.level == null) {
|
||||
return false;
|
||||
}
|
||||
Identifier id = Identifier.tryParse(blockId);
|
||||
Optional<Block> block = id == null ? Optional.empty() : BuiltInRegistries.BLOCK.getOptional(id);
|
||||
return block.isPresent() && client.level.getBlockState(pos).getBlock() == block.get();
|
||||
}
|
||||
|
||||
private static void renderProjectCells(Minecraft client, ComputerVoxelProject project, BlockPos anchor, Direction facing) {
|
||||
List<ComputerVoxelPlacement.Cell> cells = ComputerVoxelPlacement.orderedCells(project);
|
||||
int stride = Math.max(1, (int) Math.ceil(cells.size() / (double) MAX_PREVIEW_CELLS));
|
||||
for (int i = 0; i < cells.size(); i += stride) {
|
||||
ComputerVoxelPlacement.Cell cell = cells.get(i);
|
||||
BlockPos pos = ComputerVoxelPlacement.transform(project, cell, anchor, facing);
|
||||
int color = blockPreviewColor(client, cell.blockId());
|
||||
Gizmos.cuboid(new AABB(pos), GizmoStyle.strokeAndFill(withAlpha(color, 0xB8), 0.25F, withAlpha(color, 0x48)), false).setAlwaysOnTop();
|
||||
}
|
||||
}
|
||||
|
||||
private static int blockPreviewColor(Minecraft client, String blockId) {
|
||||
Integer cached = BLOCK_COLOR_CACHE.get(blockId);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Identifier id = Identifier.tryParse(blockId);
|
||||
Block block = id == null ? Blocks.AIR : BuiltInRegistries.BLOCK.getOptional(id).orElse(Blocks.AIR);
|
||||
int color = 0xFF7F7F7F;
|
||||
if (block != Blocks.AIR && client.level != null) {
|
||||
MapColor mapColor = block.defaultBlockState().getMapColor(client.level, BlockPos.ZERO);
|
||||
color = 0xFF000000 | (mapColor.calculateARGBColor(MapColor.Brightness.NORMAL) & 0x00FFFFFF);
|
||||
}
|
||||
BLOCK_COLOR_CACHE.put(blockId, color);
|
||||
return color;
|
||||
}
|
||||
|
||||
private static AABB projectBox(ComputerVoxelProject project, BlockPos anchor, Direction facing) {
|
||||
List<ComputerVoxelPlacement.Cell> cells = ComputerVoxelPlacement.orderedCells(project);
|
||||
if (cells.isEmpty()) {
|
||||
return new AABB(anchor);
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (ComputerVoxelPlacement.Cell cell : cells) {
|
||||
BlockPos pos = ComputerVoxelPlacement.transform(project, cell, anchor, facing);
|
||||
minX = Math.min(minX, pos.getX());
|
||||
minY = Math.min(minY, pos.getY());
|
||||
minZ = Math.min(minZ, pos.getZ());
|
||||
maxX = Math.max(maxX, pos.getX());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
maxZ = Math.max(maxZ, pos.getZ());
|
||||
}
|
||||
return new AABB(minX, minY, minZ, maxX + 1, maxY + 1, maxZ + 1);
|
||||
}
|
||||
|
||||
private static BlockPos resolvePreviewAnchor(Minecraft client) {
|
||||
Entity cameraEntity = client.getCameraEntity();
|
||||
if (cameraEntity == null) {
|
||||
cameraEntity = client.player;
|
||||
}
|
||||
if (cameraEntity == null) {
|
||||
return anchor;
|
||||
}
|
||||
BlockHitResult hit = resolvePreviewHit(client);
|
||||
if (hit != null) {
|
||||
return hit.getBlockPos().relative(hit.getDirection()).immutable();
|
||||
}
|
||||
Vec3 from = cameraEntity.getEyePosition();
|
||||
Vec3 to = from.add(cameraEntity.getViewVector(1.0F).scale(8.0D));
|
||||
return BlockPos.containing(to).immutable();
|
||||
}
|
||||
|
||||
private static BlockHitResult resolvePreviewHit(Minecraft client) {
|
||||
if (client.level == null) {
|
||||
return null;
|
||||
}
|
||||
Entity cameraEntity = client.getCameraEntity();
|
||||
if (cameraEntity == null) {
|
||||
cameraEntity = client.player;
|
||||
}
|
||||
if (cameraEntity == null) {
|
||||
return null;
|
||||
}
|
||||
Vec3 from = cameraEntity.getEyePosition();
|
||||
Vec3 to = from.add(cameraEntity.getViewVector(1.0F).scale(RAYCAST_DISTANCE));
|
||||
BlockHitResult hitResult = client.level.clip(new ClipContext(from, to, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, cameraEntity));
|
||||
return hitResult.getType() == HitResult.Type.BLOCK ? hitResult : null;
|
||||
}
|
||||
|
||||
private static Direction horizontal(Direction direction) {
|
||||
if (direction == Direction.UP || direction == Direction.DOWN || direction == null) {
|
||||
return Direction.NORTH;
|
||||
}
|
||||
return direction;
|
||||
}
|
||||
|
||||
private static String shortId(String id) {
|
||||
int split = id.indexOf(':');
|
||||
return split >= 0 ? id.substring(split + 1) : id;
|
||||
}
|
||||
|
||||
private static int withAlpha(int color, int alpha) {
|
||||
return (alpha << 24) | (color & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
private enum Mode {
|
||||
BLUEPRINT,
|
||||
CREATIVE_DIRECT
|
||||
}
|
||||
|
||||
private record Progress(int done, int total, ComputerVoxelPlacement.Cell current) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package fr.koka99cab.computer.client.voxel;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelProject;
|
||||
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;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
public final class ComputerVoxelGallery {
|
||||
private ComputerVoxelGallery() {
|
||||
}
|
||||
|
||||
public static List<ComputerVoxelProject> load(Minecraft minecraft) {
|
||||
Path path = galleryPath(minecraft);
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return List.of();
|
||||
}
|
||||
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
|
||||
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
List<ComputerVoxelProject> projects = new ArrayList<>();
|
||||
for (JsonElement element : array(root, "projects")) {
|
||||
if (element.isJsonObject()) {
|
||||
projects.add(project(element.getAsJsonObject()));
|
||||
}
|
||||
}
|
||||
projects.sort(Comparator.comparingLong(ComputerVoxelProject::updatedAt).reversed().thenComparing(ComputerVoxelProject::name));
|
||||
return projects;
|
||||
} catch (IOException | IllegalStateException exception) {
|
||||
ComputerMod.LOGGER.warn("Could not load Computer voxel gallery.", exception);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(Minecraft minecraft, List<ComputerVoxelProject> projects) {
|
||||
Path path = galleryPath(minecraft);
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
JsonObject root = new JsonObject();
|
||||
root.addProperty("kind", "computer-voxel-gallery");
|
||||
JsonArray array = new JsonArray();
|
||||
for (ComputerVoxelProject project : projects) {
|
||||
array.add(project(project));
|
||||
}
|
||||
root.add("projects", array);
|
||||
try (Writer writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
|
||||
writer.write(root.toString());
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
ComputerMod.LOGGER.warn("Could not save Computer voxel gallery.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path galleryPath(Minecraft minecraft) {
|
||||
Path gameDir = minecraft == null ? Path.of(".") : minecraft.gameDirectory.toPath();
|
||||
return gameDir.resolve("computer").resolve("voxel_gallery.json");
|
||||
}
|
||||
|
||||
private static ComputerVoxelProject project(JsonObject object) {
|
||||
List<String> palette = new ArrayList<>();
|
||||
for (JsonElement element : array(object, "palette")) {
|
||||
if (element.isJsonPrimitive()) {
|
||||
palette.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
Map<String, String> cells = new LinkedHashMap<>();
|
||||
JsonObject cellObject = object(object, "cells");
|
||||
for (Map.Entry<String, JsonElement> entry : cellObject.entrySet()) {
|
||||
if (entry.getValue().isJsonPrimitive()) {
|
||||
cells.put(entry.getKey(), entry.getValue().getAsString());
|
||||
}
|
||||
}
|
||||
return new ComputerVoxelProject(
|
||||
string(object, "id", "voxel-project"),
|
||||
string(object, "name", "Untitled"),
|
||||
integer(object, "width", 12),
|
||||
integer(object, "height", 12),
|
||||
integer(object, "depth", 12),
|
||||
palette,
|
||||
cells,
|
||||
number(object, "created_at", 0L),
|
||||
number(object, "updated_at", 0L)
|
||||
);
|
||||
}
|
||||
|
||||
private static JsonObject project(ComputerVoxelProject project) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("id", project.id());
|
||||
object.addProperty("name", project.name());
|
||||
object.addProperty("width", project.width());
|
||||
object.addProperty("height", project.height());
|
||||
object.addProperty("depth", project.depth());
|
||||
object.addProperty("created_at", project.createdAt());
|
||||
object.addProperty("updated_at", project.updatedAt());
|
||||
JsonArray palette = new JsonArray();
|
||||
for (String id : project.palette()) {
|
||||
palette.add(id);
|
||||
}
|
||||
object.add("palette", palette);
|
||||
JsonObject cells = new JsonObject();
|
||||
for (Map.Entry<String, String> entry : project.cells().entrySet()) {
|
||||
cells.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
object.add("cells", cells);
|
||||
return object;
|
||||
}
|
||||
|
||||
private static JsonObject object(JsonObject object, String key) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonObject() ? element.getAsJsonObject() : new JsonObject();
|
||||
}
|
||||
|
||||
private static JsonArray array(JsonObject object, String key) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonArray() ? element.getAsJsonArray() : new JsonArray();
|
||||
}
|
||||
|
||||
private static String string(JsonObject object, String key, String fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonPrimitive() ? element.getAsString() : fallback;
|
||||
}
|
||||
|
||||
private static int integer(JsonObject object, String key, int fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback;
|
||||
}
|
||||
|
||||
private static long number(JsonObject object, String key, long fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonPrimitive() ? element.getAsLong() : fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
package fr.koka99cab.computer.client.voxel;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelProject;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelPlacement;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelValidator;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ComputerVoxelProjectBuilder {
|
||||
private static final int GRID = 16;
|
||||
private static final Map<String, Integer> FALLBACK_CONCRETE_COLORS = Map.ofEntries(
|
||||
Map.entry("minecraft:white_concrete", 0xF9FFFE),
|
||||
Map.entry("minecraft:orange_concrete", 0xF9801D),
|
||||
Map.entry("minecraft:magenta_concrete", 0xC74EBD),
|
||||
Map.entry("minecraft:light_blue_concrete", 0x3AB3DA),
|
||||
Map.entry("minecraft:yellow_concrete", 0xFED83D),
|
||||
Map.entry("minecraft:lime_concrete", 0x80C71F),
|
||||
Map.entry("minecraft:pink_concrete", 0xF38BAA),
|
||||
Map.entry("minecraft:gray_concrete", 0x474F52),
|
||||
Map.entry("minecraft:light_gray_concrete", 0x9D9D97),
|
||||
Map.entry("minecraft:cyan_concrete", 0x169C9C),
|
||||
Map.entry("minecraft:purple_concrete", 0x8932B8),
|
||||
Map.entry("minecraft:blue_concrete", 0x3C44AA),
|
||||
Map.entry("minecraft:brown_concrete", 0x835432),
|
||||
Map.entry("minecraft:green_concrete", 0x5E7C16),
|
||||
Map.entry("minecraft:red_concrete", 0xB02E26),
|
||||
Map.entry("minecraft:black_concrete", 0x1D1D21)
|
||||
);
|
||||
|
||||
private ComputerVoxelProjectBuilder() {
|
||||
}
|
||||
|
||||
public enum BuildMode {
|
||||
FACE("face", "Face"),
|
||||
HOLLOW("hollow", "Vide"),
|
||||
FULL("full", "Plein");
|
||||
|
||||
private final String id;
|
||||
private final String label;
|
||||
|
||||
BuildMode(String id, String label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public BuildMode next() {
|
||||
BuildMode[] values = values();
|
||||
return values[(ordinal() + 1) % values.length];
|
||||
}
|
||||
}
|
||||
|
||||
public static ComputerVoxelProject build(String entryId, String displayName) {
|
||||
return build(entryId, displayName, BuildMode.FACE, List.of());
|
||||
}
|
||||
|
||||
public static ComputerVoxelProject build(String entryId, String displayName, BuildMode mode) {
|
||||
return build(entryId, displayName, mode, List.of());
|
||||
}
|
||||
|
||||
public static ComputerVoxelProject build(String entryId, String displayName, BuildMode mode, List<String> paletteBlockIds) {
|
||||
Identifier id = Identifier.tryParse(entryId);
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("Identifiant invalide: " + entryId);
|
||||
}
|
||||
|
||||
ResourceManager resources = Minecraft.getInstance().getResourceManager();
|
||||
ColorPalette colorPalette = ColorPalette.from(paletteBlockIds);
|
||||
Optional<Block> block = BuiltInRegistries.BLOCK.getOptional(id);
|
||||
if (block.isPresent()) {
|
||||
return blockProject(resources, id, block.get(), displayName, mode == null ? BuildMode.FACE : mode, colorPalette);
|
||||
}
|
||||
|
||||
Optional<Item> item = BuiltInRegistries.ITEM.getOptional(id);
|
||||
if (item.isPresent() && item.get() != Items.AIR) {
|
||||
return itemProject(resources, id, displayName, colorPalette);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Entree non voxelisable pour l'instant: " + entryId);
|
||||
}
|
||||
|
||||
private static ComputerVoxelProject blockProject(ResourceManager resources, Identifier id, Block block, String displayName, BuildMode mode, ColorPalette colorPalette) {
|
||||
String blockId = id.toString();
|
||||
if (!ComputerVoxelValidator.isBuildableFullBlock(blockId)) {
|
||||
throw new IllegalArgumentException("Bloc non constructible plein: " + blockId);
|
||||
}
|
||||
colorPalette.requireUsable();
|
||||
|
||||
Map<String, String> cells = new LinkedHashMap<>();
|
||||
Set<String> palette = new LinkedHashSet<>();
|
||||
Map<String, BufferedImage> textureCache = new LinkedHashMap<>();
|
||||
List<JsonObject> models = blockModels(resources, id);
|
||||
if (models.isEmpty()) {
|
||||
addFallbackBlock(resources, id, cells, palette, textureCache, mode, colorPalette);
|
||||
} else {
|
||||
for (JsonObject model : models) {
|
||||
JsonArray elements = array(model, "elements");
|
||||
if (elements.isEmpty()) {
|
||||
addFallbackBlock(resources, id, cells, palette, textureCache, mode, colorPalette);
|
||||
continue;
|
||||
}
|
||||
for (JsonElement element : elements) {
|
||||
if (!element.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
addTexturedElement(resources, model, element.getAsJsonObject(), cells, palette, textureCache, mode, colorPalette);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cells.isEmpty()) {
|
||||
throw new IllegalArgumentException("Aucun voxel opaque a generer: " + blockId);
|
||||
}
|
||||
|
||||
return new ComputerVoxelProject(
|
||||
projectId(id, mode),
|
||||
projectName(id, displayName, mode),
|
||||
GRID,
|
||||
GRID,
|
||||
mode == BuildMode.FACE ? 1 : GRID,
|
||||
List.copyOf(palette),
|
||||
cells,
|
||||
0L,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
}
|
||||
|
||||
private static ComputerVoxelProject itemProject(ResourceManager resources, Identifier id, String displayName, ColorPalette colorPalette) {
|
||||
colorPalette.requireUsable();
|
||||
JsonObject itemModel = itemModel(resources, id);
|
||||
JsonObject textures = object(itemModel, "textures");
|
||||
String textureRef = resolveTexture(textures, "#layer0");
|
||||
if (textureRef == null) {
|
||||
textureRef = resolveTexture(textures, "#all");
|
||||
}
|
||||
if (textureRef == null) {
|
||||
textureRef = resolveTexture(textures, "#particle");
|
||||
}
|
||||
if (textureRef == null) {
|
||||
throw new IllegalArgumentException("Texture item introuvable: " + id);
|
||||
}
|
||||
|
||||
BufferedImage image = readTexture(resources, textureRef);
|
||||
Map<String, String> cells = new LinkedHashMap<>();
|
||||
Set<String> palette = new LinkedHashSet<>();
|
||||
for (int x = 0; x < GRID; x++) {
|
||||
for (int y = 0; y < GRID; y++) {
|
||||
int sampleX = Math.min(image.getWidth() - 1, x * image.getWidth() / GRID);
|
||||
int sampleY = Math.min(image.getHeight() - 1, y * image.getHeight() / GRID);
|
||||
int argb = image.getRGB(sampleX, sampleY);
|
||||
int alpha = (argb >>> 24) & 0xFF;
|
||||
if (alpha < 24) {
|
||||
continue;
|
||||
}
|
||||
String blockId = colorPalette.nearest(argb & 0xFFFFFF);
|
||||
palette.add(blockId);
|
||||
cells.put(ComputerVoxelPlacement.cellKey(x, GRID - 1 - y, 0), blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (cells.isEmpty()) {
|
||||
throw new IllegalArgumentException("Aucun pixel opaque a voxeliser: " + id);
|
||||
}
|
||||
|
||||
return new ComputerVoxelProject(
|
||||
projectId(id, BuildMode.FACE),
|
||||
displayName == null || displayName.isBlank() ? title(id) : displayName,
|
||||
GRID,
|
||||
GRID,
|
||||
1,
|
||||
List.copyOf(palette),
|
||||
cells,
|
||||
0L,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
}
|
||||
|
||||
private static List<JsonObject> blockModels(ResourceManager resources, Identifier id) {
|
||||
JsonObject blockstate = readJson(resources, Identifier.fromNamespaceAndPath(id.getNamespace(), "blockstates/" + id.getPath() + ".json"));
|
||||
List<String> refs = new ArrayList<>();
|
||||
JsonObject variants = object(blockstate, "variants");
|
||||
if (!variants.entrySet().isEmpty()) {
|
||||
JsonElement first = variants.entrySet().iterator().next().getValue();
|
||||
addModelRefs(refs, first);
|
||||
}
|
||||
JsonArray multipart = array(blockstate, "multipart");
|
||||
for (JsonElement element : multipart) {
|
||||
if (element.isJsonObject()) {
|
||||
addModelRefs(refs, element.getAsJsonObject().get("apply"));
|
||||
}
|
||||
}
|
||||
|
||||
List<JsonObject> models = new ArrayList<>();
|
||||
for (String ref : refs) {
|
||||
resolveModel(resources, ref, new LinkedHashSet<>()).ifPresent(models::add);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
private static JsonObject itemModel(ResourceManager resources, Identifier id) {
|
||||
String modelRef = null;
|
||||
Optional<JsonObject> itemDefinition = maybeReadJson(resources, Identifier.fromNamespaceAndPath(id.getNamespace(), "items/" + id.getPath() + ".json"));
|
||||
if (itemDefinition.isPresent()) {
|
||||
JsonObject model = object(itemDefinition.get(), "model");
|
||||
if ("minecraft:model".equals(string(model, "type", ""))) {
|
||||
modelRef = string(model, "model", null);
|
||||
}
|
||||
}
|
||||
if (modelRef == null || modelRef.isBlank()) {
|
||||
modelRef = id.getNamespace() + ":item/" + id.getPath();
|
||||
}
|
||||
String resolvedModelRef = modelRef;
|
||||
return resolveModel(resources, resolvedModelRef, new LinkedHashSet<>())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Modele item introuvable: " + resolvedModelRef));
|
||||
}
|
||||
|
||||
private static void addModelRefs(List<String> refs, JsonElement element) {
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
if (element.isJsonArray()) {
|
||||
for (JsonElement child : element.getAsJsonArray()) {
|
||||
addModelRefs(refs, child);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!element.isJsonObject()) {
|
||||
return;
|
||||
}
|
||||
String model = string(element.getAsJsonObject(), "model", "");
|
||||
if (!model.isBlank()) {
|
||||
refs.add(model);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<JsonObject> resolveModel(ResourceManager resources, String ref, Set<String> seen) {
|
||||
Ref split = splitRef(ref);
|
||||
String key = split.namespace() + ":" + split.path();
|
||||
if (!seen.add(key)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<JsonObject> maybeRaw = maybeReadJson(resources, Identifier.fromNamespaceAndPath(split.namespace(), "models/" + split.path() + ".json"));
|
||||
if (maybeRaw.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
JsonObject raw = maybeRaw.get();
|
||||
JsonObject merged = new JsonObject();
|
||||
JsonObject textures = new JsonObject();
|
||||
String parent = string(raw, "parent", "");
|
||||
if (!parent.isBlank()) {
|
||||
resolveModel(resources, parent, seen).ifPresent(parentModel -> {
|
||||
copyObject(object(parentModel, "textures"), textures);
|
||||
if (parentModel.has("elements")) {
|
||||
merged.add("elements", parentModel.get("elements").deepCopy());
|
||||
}
|
||||
});
|
||||
}
|
||||
copyObject(object(raw, "textures"), textures);
|
||||
merged.add("textures", textures);
|
||||
if (raw.has("elements")) {
|
||||
merged.add("elements", raw.get("elements").deepCopy());
|
||||
}
|
||||
return Optional.of(merged);
|
||||
}
|
||||
|
||||
private static void addFallbackBlock(ResourceManager resources, Identifier id, Map<String, String> cells, Set<String> palette, Map<String, BufferedImage> textureCache, BuildMode mode, ColorPalette colorPalette) {
|
||||
JsonObject model = new JsonObject();
|
||||
JsonObject textures = new JsonObject();
|
||||
textures.addProperty("all", id.getNamespace() + ":block/" + id.getPath());
|
||||
textures.addProperty("particle", "#" + "all");
|
||||
model.add("textures", textures);
|
||||
JsonObject element = new JsonObject();
|
||||
element.add("from", coordsArray(0, 0, 0));
|
||||
element.add("to", coordsArray(GRID, GRID, GRID));
|
||||
JsonObject faces = new JsonObject();
|
||||
for (String face : List.of("north", "south", "east", "west", "up", "down")) {
|
||||
JsonObject faceObject = new JsonObject();
|
||||
faceObject.addProperty("texture", "#all");
|
||||
faces.add(face, faceObject);
|
||||
}
|
||||
element.add("faces", faces);
|
||||
addTexturedElement(resources, model, element, cells, palette, textureCache, mode, colorPalette);
|
||||
}
|
||||
|
||||
private static void addTexturedElement(
|
||||
ResourceManager resources,
|
||||
JsonObject model,
|
||||
JsonObject element,
|
||||
Map<String, String> cells,
|
||||
Set<String> palette,
|
||||
Map<String, BufferedImage> textureCache,
|
||||
BuildMode mode,
|
||||
ColorPalette colorPalette
|
||||
) {
|
||||
int[] from = coords(element.get("from"), 0, true);
|
||||
int[] to = coords(element.get("to"), GRID, false);
|
||||
int startX = clamp(Math.min(from[0], to[0]));
|
||||
int startY = clamp(Math.min(from[1], to[1]));
|
||||
int startZ = clamp(Math.min(from[2], to[2]));
|
||||
int endX = clamp(Math.max(from[0], to[0]));
|
||||
int endY = clamp(Math.max(from[1], to[1]));
|
||||
int endZ = clamp(Math.max(from[2], to[2]));
|
||||
JsonObject faces = object(element, "faces");
|
||||
if (mode == BuildMode.FACE) {
|
||||
addProjectedFace(resources, model, faces, startX, startY, startZ, endX, endY, endZ, cells, palette, textureCache, colorPalette);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int x = startX; x < endX; x++) {
|
||||
for (int y = startY; y < endY; y++) {
|
||||
for (int z = startZ; z < endZ; z++) {
|
||||
if (mode == BuildMode.HOLLOW && !isBoundary(x, y, z, startX, startY, startZ, endX, endY, endZ)) {
|
||||
continue;
|
||||
}
|
||||
String face = nearestFace(x, y, z, startX, startY, startZ, endX, endY, endZ);
|
||||
String blockId = blockForFace(resources, model, faces, face, x, y, z, startX, startY, startZ, endX, endY, endZ, textureCache, colorPalette);
|
||||
if (blockId != null) {
|
||||
palette.add(blockId);
|
||||
cells.put(ComputerVoxelPlacement.cellKey(x, y, z), blockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addProjectedFace(
|
||||
ResourceManager resources,
|
||||
JsonObject model,
|
||||
JsonObject faces,
|
||||
int startX,
|
||||
int startY,
|
||||
int startZ,
|
||||
int endX,
|
||||
int endY,
|
||||
int endZ,
|
||||
Map<String, String> cells,
|
||||
Set<String> palette,
|
||||
Map<String, BufferedImage> textureCache,
|
||||
ColorPalette colorPalette
|
||||
) {
|
||||
String face = projectedFace(faces);
|
||||
for (int x = startX; x < endX; x++) {
|
||||
for (int y = startY; y < endY; y++) {
|
||||
int sourceZ = face.equals("south") ? endZ - 1 : startZ;
|
||||
String blockId = blockForFace(resources, model, faces, face, x, y, sourceZ, startX, startY, startZ, endX, endY, endZ, textureCache, colorPalette);
|
||||
if (blockId != null) {
|
||||
palette.add(blockId);
|
||||
cells.put(ComputerVoxelPlacement.cellKey(x, y, 0), blockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String projectedFace(JsonObject faces) {
|
||||
for (String face : List.of("north", "south", "east", "west", "up", "down")) {
|
||||
if (faces.has(face)) {
|
||||
return face;
|
||||
}
|
||||
}
|
||||
return "north";
|
||||
}
|
||||
|
||||
private static String blockForFace(
|
||||
ResourceManager resources,
|
||||
JsonObject model,
|
||||
JsonObject faces,
|
||||
String face,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int startX,
|
||||
int startY,
|
||||
int startZ,
|
||||
int endX,
|
||||
int endY,
|
||||
int endZ,
|
||||
Map<String, BufferedImage> textureCache,
|
||||
ColorPalette colorPalette
|
||||
) {
|
||||
JsonObject faceObject = object(faces, face);
|
||||
String textureRef = faceTexture(model, faces, face);
|
||||
if (textureRef == null) {
|
||||
return null;
|
||||
}
|
||||
BufferedImage image = readTextureCached(resources, textureRef, textureCache);
|
||||
int[] uv = uv(faceObject);
|
||||
int[] faceUv = faceUv(face, x, y, z, startX, startY, startZ, endX, endY, endZ);
|
||||
int argb = sampleTexture(image, uv, faceUv[0], faceUv[1]);
|
||||
int alpha = (argb >>> 24) & 0xFF;
|
||||
return alpha < 24 ? null : colorPalette.nearest(argb & 0xFFFFFF);
|
||||
}
|
||||
|
||||
private static String faceTexture(JsonObject model, JsonObject faces, String face) {
|
||||
JsonObject textures = object(model, "textures");
|
||||
JsonObject faceObject = object(faces, face);
|
||||
String ref = string(faceObject, "texture", "");
|
||||
String texture = resolveTexture(textures, ref);
|
||||
if (texture != null) {
|
||||
return texture;
|
||||
}
|
||||
for (String fallback : textureFallbacks(face)) {
|
||||
texture = resolveTexture(textures, fallback);
|
||||
if (texture != null) {
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<String> textureFallbacks(String face) {
|
||||
return switch (face) {
|
||||
case "up" -> List.of("#up", "#top", "#end", "#all", "#side", "#particle");
|
||||
case "down" -> List.of("#down", "#bottom", "#end", "#all", "#side", "#particle");
|
||||
default -> List.of("#" + face, "#side", "#all", "#particle", "#top");
|
||||
};
|
||||
}
|
||||
|
||||
private static int[] uv(JsonObject faceObject) {
|
||||
JsonArray uv = array(faceObject, "uv");
|
||||
if (uv.size() >= 4) {
|
||||
return new int[]{
|
||||
clampTexture((int) Math.floor(uv.get(0).getAsDouble())),
|
||||
clampTexture((int) Math.floor(uv.get(1).getAsDouble())),
|
||||
clampTexture((int) Math.ceil(uv.get(2).getAsDouble())),
|
||||
clampTexture((int) Math.ceil(uv.get(3).getAsDouble()))
|
||||
};
|
||||
}
|
||||
return new int[]{0, 0, GRID, GRID};
|
||||
}
|
||||
|
||||
private static int[] faceUv(String face, int x, int y, int z, int startX, int startY, int startZ, int endX, int endY, int endZ) {
|
||||
int localX = scaleToGrid(x - startX, endX - startX);
|
||||
int localY = scaleToGrid(y - startY, endY - startY);
|
||||
int localZ = scaleToGrid(z - startZ, endZ - startZ);
|
||||
return switch (face) {
|
||||
case "south" -> new int[]{localX, GRID - 1 - localY};
|
||||
case "east" -> new int[]{localZ, GRID - 1 - localY};
|
||||
case "west" -> new int[]{GRID - 1 - localZ, GRID - 1 - localY};
|
||||
case "up" -> new int[]{localX, localZ};
|
||||
case "down" -> new int[]{localX, GRID - 1 - localZ};
|
||||
default -> new int[]{GRID - 1 - localX, GRID - 1 - localY};
|
||||
};
|
||||
}
|
||||
|
||||
private static int sampleTexture(BufferedImage image, int[] uv, int uIndex, int vIndex) {
|
||||
double uStart = uv[0];
|
||||
double vStart = uv[1];
|
||||
double uEnd = uv[2];
|
||||
double vEnd = uv[3];
|
||||
double u = uStart + (uIndex + 0.5D) * (uEnd - uStart) / GRID;
|
||||
double v = vStart + (vIndex + 0.5D) * (vEnd - vStart) / GRID;
|
||||
int sampleX = Math.max(0, Math.min(image.getWidth() - 1, (int) Math.floor(u * image.getWidth() / GRID)));
|
||||
int sampleY = Math.max(0, Math.min(image.getHeight() - 1, (int) Math.floor(v * image.getHeight() / GRID)));
|
||||
return image.getRGB(sampleX, sampleY);
|
||||
}
|
||||
|
||||
private static boolean isBoundary(int x, int y, int z, int startX, int startY, int startZ, int endX, int endY, int endZ) {
|
||||
return x == startX || x == endX - 1 || y == startY || y == endY - 1 || z == startZ || z == endZ - 1;
|
||||
}
|
||||
|
||||
private static String nearestFace(int x, int y, int z, int startX, int startY, int startZ, int endX, int endY, int endZ) {
|
||||
int bestDistance = z - startZ;
|
||||
String best = "north";
|
||||
int south = endZ - 1 - z;
|
||||
if (south < bestDistance) {
|
||||
bestDistance = south;
|
||||
best = "south";
|
||||
}
|
||||
int west = x - startX;
|
||||
if (west < bestDistance) {
|
||||
bestDistance = west;
|
||||
best = "west";
|
||||
}
|
||||
int east = endX - 1 - x;
|
||||
if (east < bestDistance) {
|
||||
bestDistance = east;
|
||||
best = "east";
|
||||
}
|
||||
int down = y - startY;
|
||||
if (down < bestDistance) {
|
||||
bestDistance = down;
|
||||
best = "down";
|
||||
}
|
||||
int up = endY - 1 - y;
|
||||
if (up < bestDistance) {
|
||||
best = "up";
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int[] coords(JsonElement element, int fallback, boolean min) {
|
||||
if (element == null || !element.isJsonArray() || element.getAsJsonArray().size() < 3) {
|
||||
return new int[]{fallback, fallback, fallback};
|
||||
}
|
||||
JsonArray values = element.getAsJsonArray();
|
||||
return new int[]{
|
||||
clamp((int) (min ? Math.floor(values.get(0).getAsDouble()) : Math.ceil(values.get(0).getAsDouble()))),
|
||||
clamp((int) (min ? Math.floor(values.get(1).getAsDouble()) : Math.ceil(values.get(1).getAsDouble()))),
|
||||
clamp((int) (min ? Math.floor(values.get(2).getAsDouble()) : Math.ceil(values.get(2).getAsDouble())))
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonArray coordsArray(int x, int y, int z) {
|
||||
JsonArray array = new JsonArray();
|
||||
array.add(x);
|
||||
array.add(y);
|
||||
array.add(z);
|
||||
return array;
|
||||
}
|
||||
|
||||
private static BufferedImage readTextureCached(ResourceManager resources, String textureRef, Map<String, BufferedImage> cache) {
|
||||
BufferedImage cached = cache.get(textureRef);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
BufferedImage image = readTexture(resources, textureRef);
|
||||
cache.put(textureRef, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static BufferedImage readTexture(ResourceManager resources, String textureRef) {
|
||||
Ref split = splitRef(textureRef);
|
||||
Identifier textureId = Identifier.fromNamespaceAndPath(split.namespace(), "textures/" + split.path() + ".png");
|
||||
Resource resource = resources.getResource(textureId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Texture introuvable: " + textureRef));
|
||||
try (InputStream stream = resource.open()) {
|
||||
BufferedImage image = ImageIO.read(stream);
|
||||
if (image == null) {
|
||||
throw new IllegalArgumentException("Texture illisible: " + textureRef);
|
||||
}
|
||||
return image;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalArgumentException("Texture illisible: " + textureRef, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject readJson(ResourceManager resources, Identifier id) {
|
||||
return maybeReadJson(resources, id).orElseThrow(() -> new IllegalArgumentException("Asset introuvable: " + id));
|
||||
}
|
||||
|
||||
private static Optional<JsonObject> maybeReadJson(ResourceManager resources, Identifier id) {
|
||||
Optional<Resource> resource = resources.getResource(id);
|
||||
if (resource.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try (Reader reader = resource.get().openAsReader()) {
|
||||
JsonElement parsed = JsonParser.parseReader(reader);
|
||||
return parsed.isJsonObject() ? Optional.of(parsed.getAsJsonObject()) : Optional.empty();
|
||||
} catch (IOException | IllegalStateException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveTexture(JsonObject textures, String ref) {
|
||||
if (ref == null || ref.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String current = ref;
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
while (current.startsWith("#")) {
|
||||
String key = current.substring(1);
|
||||
if (!seen.add(key) || !textures.has(key) || !textures.get(key).isJsonPrimitive()) {
|
||||
return null;
|
||||
}
|
||||
current = textures.get(key).getAsString();
|
||||
}
|
||||
Ref split = splitRef(current);
|
||||
return split.namespace() + ":" + split.path();
|
||||
}
|
||||
|
||||
private static int fallbackColor(String blockId) {
|
||||
Integer concrete = FALLBACK_CONCRETE_COLORS.get(blockId);
|
||||
if (concrete != null) {
|
||||
return concrete;
|
||||
}
|
||||
Identifier id = Identifier.tryParse(blockId);
|
||||
if (id == null) {
|
||||
return 0x7F7F7F;
|
||||
}
|
||||
Block block = BuiltInRegistries.BLOCK.getOptional(id).orElse(null);
|
||||
if (block == null || Minecraft.getInstance().level == null) {
|
||||
return 0x7F7F7F;
|
||||
}
|
||||
MapColor mapColor = block.defaultBlockState().getMapColor(Minecraft.getInstance().level, BlockPos.ZERO);
|
||||
return mapColor.calculateARGBColor(MapColor.Brightness.NORMAL) & 0xFFFFFF;
|
||||
}
|
||||
|
||||
private static String nearestColor(int rgb, List<PaletteEntry> entries) {
|
||||
String best = entries.getFirst().blockId();
|
||||
long bestDistance = Long.MAX_VALUE;
|
||||
int r = (rgb >> 16) & 0xFF;
|
||||
int g = (rgb >> 8) & 0xFF;
|
||||
int b = rgb & 0xFF;
|
||||
for (PaletteEntry entry : entries) {
|
||||
int color = entry.rgb();
|
||||
long dr = r - ((color >> 16) & 0xFF);
|
||||
long dg = g - ((color >> 8) & 0xFF);
|
||||
long db = b - (color & 0xFF);
|
||||
long distance = dr * dr + dg * dg + db * db;
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = entry.blockId();
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static JsonObject object(JsonObject object, String key) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonObject() ? element.getAsJsonObject() : new JsonObject();
|
||||
}
|
||||
|
||||
private static JsonArray array(JsonObject object, String key) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonArray() ? element.getAsJsonArray() : new JsonArray();
|
||||
}
|
||||
|
||||
private static String string(JsonObject object, String key, String fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonPrimitive() ? element.getAsString() : fallback;
|
||||
}
|
||||
|
||||
private static void copyObject(JsonObject source, JsonObject target) {
|
||||
for (Map.Entry<String, JsonElement> entry : source.entrySet()) {
|
||||
target.add(entry.getKey(), entry.getValue().deepCopy());
|
||||
}
|
||||
}
|
||||
|
||||
private static int clamp(int value) {
|
||||
return Math.max(0, Math.min(GRID, value));
|
||||
}
|
||||
|
||||
private static int clampTexture(int value) {
|
||||
return Math.max(0, Math.min(GRID, value));
|
||||
}
|
||||
|
||||
private static int scaleToGrid(int value, int span) {
|
||||
if (span <= 1) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.min(GRID - 1, value * GRID / span));
|
||||
}
|
||||
|
||||
private static String projectId(Identifier id, BuildMode mode) {
|
||||
return "asset/" + id.getNamespace() + "/" + id.getPath() + "/" + mode.id();
|
||||
}
|
||||
|
||||
private static String projectName(Identifier id, String displayName, BuildMode mode) {
|
||||
String base = displayName == null || displayName.isBlank() ? title(id) : displayName;
|
||||
return base + " (" + mode.label() + ")";
|
||||
}
|
||||
|
||||
private static String title(Identifier id) {
|
||||
String[] parts = id.getPath().replace('/', '_').replace('_', ' ').split("\\s+");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String part : parts) {
|
||||
if (part.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(' ');
|
||||
}
|
||||
builder.append(Character.toUpperCase(part.charAt(0))).append(part.substring(1));
|
||||
}
|
||||
return builder.isEmpty() ? id.toString() : builder.toString();
|
||||
}
|
||||
|
||||
private static Ref splitRef(String ref) {
|
||||
String clean = ref == null ? "" : ref;
|
||||
int split = clean.indexOf(':');
|
||||
if (split >= 0) {
|
||||
return new Ref(clean.substring(0, split), clean.substring(split + 1));
|
||||
}
|
||||
return new Ref("minecraft", clean);
|
||||
}
|
||||
|
||||
private record Ref(String namespace, String path) {
|
||||
}
|
||||
|
||||
private record ColorPalette(List<PaletteEntry> entries) {
|
||||
private static ColorPalette from(List<String> blockIds) {
|
||||
List<PaletteEntry> entries = new ArrayList<>();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
for (String blockId : blockIds == null ? List.<String>of() : blockIds) {
|
||||
if (blockId == null || blockId.isBlank() || !seen.add(blockId) || !ComputerVoxelValidator.isOpaquePaletteBlock(blockId)) {
|
||||
continue;
|
||||
}
|
||||
entries.add(new PaletteEntry(blockId, fallbackColor(blockId)));
|
||||
}
|
||||
return new ColorPalette(List.copyOf(entries));
|
||||
}
|
||||
|
||||
private void requireUsable() {
|
||||
if (entries.isEmpty()) {
|
||||
throw new IllegalArgumentException("Palette Minecraft vide: decouvre des blocs dans Blocotech.");
|
||||
}
|
||||
}
|
||||
|
||||
private String nearest(int rgb) {
|
||||
return nearestColor(rgb, entries);
|
||||
}
|
||||
}
|
||||
|
||||
private record PaletteEntry(String blockId, int rgb) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.computer;
|
||||
|
||||
import fr.koka99cab.computer.command.ComputerCommands;
|
||||
import fr.koka99cab.computer.network.ComputerOpenPayload;
|
||||
import fr.koka99cab.computer.network.ComputerPlaceVoxelProjectPayload;
|
||||
import fr.koka99cab.computer.network.ComputerStatsPayload;
|
||||
import fr.koka99cab.computer.network.ComputerStatsRequestPayload;
|
||||
import fr.koka99cab.computer.registry.ComputerBlocks;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelPlacementHandler;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class ComputerMod implements ModInitializer {
|
||||
public static final String MOD_ID = "computer";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
ComputerBlocks.initialize();
|
||||
ComputerCommands.register();
|
||||
PayloadTypeRegistry.clientboundPlay().register(ComputerOpenPayload.TYPE, ComputerOpenPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(ComputerStatsPayload.TYPE, ComputerStatsPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ComputerStatsRequestPayload.TYPE, ComputerStatsRequestPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ComputerPlaceVoxelProjectPayload.TYPE, ComputerPlaceVoxelProjectPayload.STREAM_CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(ComputerStatsRequestPayload.TYPE, (payload, context) ->
|
||||
context.server().execute(() -> {
|
||||
if (ServerPlayNetworking.canSend(context.player(), ComputerStatsPayload.TYPE)) {
|
||||
ServerPlayNetworking.send(context.player(), ComputerStatsPayload.from(context.player()));
|
||||
}
|
||||
}));
|
||||
ServerPlayNetworking.registerGlobalReceiver(ComputerPlaceVoxelProjectPayload.TYPE, (payload, context) ->
|
||||
context.server().execute(() -> ComputerVoxelPlacementHandler.handle(context.player(), payload)));
|
||||
LOGGER.info("Computer desktop initialized.");
|
||||
}
|
||||
|
||||
public static Identifier id(String path) {
|
||||
return Identifier.fromNamespaceAndPath(MOD_ID, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka99cab.computer.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka99cab.computer.network.ComputerOpenPayload;
|
||||
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.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class ComputerBlock extends HorizontalDirectionalBlock {
|
||||
public static final MapCodec<ComputerBlock> CODEC = simpleCodec(ComputerBlock::new);
|
||||
|
||||
public ComputerBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends HorizontalDirectionalBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
|
||||
if (player instanceof ServerPlayer serverPlayer && ServerPlayNetworking.canSend(serverPlayer, ComputerOpenPayload.TYPE)) {
|
||||
ServerPlayNetworking.send(serverPlayer, new ComputerOpenPayload());
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState rotate(BlockState state, Rotation rotation) {
|
||||
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState mirror(BlockState state, Mirror mirror) {
|
||||
return state.rotate(mirror.getRotation(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka99cab.computer.command;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.permissions.Permissions;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
public final class ComputerCommands {
|
||||
private ComputerCommands() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> register(dispatcher));
|
||||
}
|
||||
|
||||
private static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(Commands.literal(ComputerMod.MOD_ID)
|
||||
.then(Commands.literal("unlock_all_blocks")
|
||||
.requires(source -> source.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
|
||||
.executes(context -> unlockAllBlocks(context.getSource())))
|
||||
.then(Commands.literal("unlock_all")
|
||||
.requires(source -> source.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
|
||||
.executes(context -> unlockAllBlocks(context.getSource())))
|
||||
.then(Commands.literal("cheat")
|
||||
.requires(source -> source.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
|
||||
.then(Commands.literal("unlock_all")
|
||||
.executes(context -> unlockAllBlocks(context.getSource())))
|
||||
.then(Commands.literal("discover_all")
|
||||
.executes(context -> unlockAllBlocks(context.getSource())))));
|
||||
}
|
||||
|
||||
private static int unlockAllBlocks(CommandSourceStack source) throws CommandSyntaxException {
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
int unlocked = 0;
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
if (block == Blocks.AIR || block.defaultBlockState().isAir()) {
|
||||
continue;
|
||||
}
|
||||
player.awardStat(Stats.BLOCK_MINED.get(block), 1);
|
||||
Item item = block.asItem();
|
||||
if (item != Items.AIR) {
|
||||
player.awardStat(Stats.ITEM_PICKED_UP.get(item), 1);
|
||||
}
|
||||
unlocked++;
|
||||
}
|
||||
player.getStats().sendStats(player);
|
||||
int count = unlocked;
|
||||
source.sendSuccess(() -> Component.literal("Computer: " + count + " blocs debloques."), true);
|
||||
return unlocked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka99cab.computer.network;
|
||||
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
public record ComputerOpenPayload() implements CustomPacketPayload {
|
||||
public static final Type<ComputerOpenPayload> TYPE = new Type<>(ComputerMod.id("open_desktop"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerOpenPayload> STREAM_CODEC = StreamCodec.unit(new ComputerOpenPayload());
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package fr.koka99cab.computer.network;
|
||||
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import fr.koka99cab.computer.voxel.ComputerVoxelProject;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
public record ComputerPlaceVoxelProjectPayload(ComputerVoxelProject project, BlockPos anchor, Direction facing, String mode) implements CustomPacketPayload {
|
||||
public static final String MODE_CREATIVE_DIRECT = "creative_direct";
|
||||
public static final String MODE_UNDO = "undo";
|
||||
public static final String MODE_REDO = "redo";
|
||||
public static final Type<ComputerPlaceVoxelProjectPayload> TYPE = new Type<>(ComputerMod.id("place_voxel_project"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerPlaceVoxelProjectPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.fromCodec(ComputerVoxelProject.CODEC),
|
||||
ComputerPlaceVoxelProjectPayload::project,
|
||||
BlockPos.STREAM_CODEC,
|
||||
ComputerPlaceVoxelProjectPayload::anchor,
|
||||
Direction.STREAM_CODEC,
|
||||
ComputerPlaceVoxelProjectPayload::facing,
|
||||
ByteBufCodecs.STRING_UTF8,
|
||||
ComputerPlaceVoxelProjectPayload::mode,
|
||||
ComputerPlaceVoxelProjectPayload::new
|
||||
);
|
||||
|
||||
public ComputerPlaceVoxelProjectPayload {
|
||||
project = project == null ? ComputerVoxelProject.empty() : project;
|
||||
anchor = anchor == null ? BlockPos.ZERO : anchor.immutable();
|
||||
facing = facing == null ? Direction.NORTH : facing;
|
||||
mode = mode == null ? "" : mode.strip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package fr.koka99cab.computer.network;
|
||||
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.ServerStatsCounter;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
public record ComputerStatsPayload(
|
||||
long playTimeTicks,
|
||||
long distanceCm,
|
||||
long minedBlocks,
|
||||
long craftedItems,
|
||||
long pickedBlocks,
|
||||
long usedItems,
|
||||
long droppedItems,
|
||||
long brokenItems,
|
||||
long placedBlocks,
|
||||
long mobKills,
|
||||
long killedByMobs,
|
||||
long deaths,
|
||||
long damageTaken,
|
||||
long jumps,
|
||||
int health,
|
||||
int maxHealth,
|
||||
int xpLevel,
|
||||
int totalExperience,
|
||||
long familyMinerais,
|
||||
long familyBois,
|
||||
long familyRoche,
|
||||
long familySols,
|
||||
long familyVegetal,
|
||||
long familyConstruits,
|
||||
long familyAutres
|
||||
) implements CustomPacketPayload {
|
||||
public static final Type<ComputerStatsPayload> TYPE = new Type<>(ComputerMod.id("stats_snapshot"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerStatsPayload> STREAM_CODEC = StreamCodec.ofMember(ComputerStatsPayload::write, ComputerStatsPayload::read);
|
||||
|
||||
public static ComputerStatsPayload from(ServerPlayer player) {
|
||||
ServerStatsCounter stats = player.getStats();
|
||||
FamilyCounts families = pickedBlockFamilies(stats);
|
||||
return new ComputerStatsPayload(
|
||||
customStat(stats, Stats.PLAY_TIME),
|
||||
distanceCentimeters(stats),
|
||||
minedBlocks(stats),
|
||||
craftedItems(stats),
|
||||
pickedBlockItems(stats),
|
||||
usedItems(stats),
|
||||
droppedItems(stats),
|
||||
brokenItems(stats),
|
||||
usedBlockItems(stats),
|
||||
customStat(stats, Stats.MOB_KILLS),
|
||||
killedByMobs(stats),
|
||||
customStat(stats, Stats.DEATHS),
|
||||
customStat(stats, Stats.DAMAGE_TAKEN),
|
||||
customStat(stats, Stats.JUMP),
|
||||
Math.round(player.getHealth()),
|
||||
Math.max(1, Math.round(player.getMaxHealth())),
|
||||
player.experienceLevel,
|
||||
player.totalExperience,
|
||||
families.minerais,
|
||||
families.bois,
|
||||
families.roche,
|
||||
families.sols,
|
||||
families.vegetal,
|
||||
families.construits,
|
||||
families.autres
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
private static ComputerStatsPayload read(RegistryFriendlyByteBuf buf) {
|
||||
return new ComputerStatsPayload(
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarInt(),
|
||||
buf.readVarInt(),
|
||||
buf.readVarInt(),
|
||||
buf.readVarInt(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong(),
|
||||
buf.readVarLong()
|
||||
);
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buf) {
|
||||
buf.writeVarLong(playTimeTicks);
|
||||
buf.writeVarLong(distanceCm);
|
||||
buf.writeVarLong(minedBlocks);
|
||||
buf.writeVarLong(craftedItems);
|
||||
buf.writeVarLong(pickedBlocks);
|
||||
buf.writeVarLong(usedItems);
|
||||
buf.writeVarLong(droppedItems);
|
||||
buf.writeVarLong(brokenItems);
|
||||
buf.writeVarLong(placedBlocks);
|
||||
buf.writeVarLong(mobKills);
|
||||
buf.writeVarLong(killedByMobs);
|
||||
buf.writeVarLong(deaths);
|
||||
buf.writeVarLong(damageTaken);
|
||||
buf.writeVarLong(jumps);
|
||||
buf.writeVarInt(health);
|
||||
buf.writeVarInt(maxHealth);
|
||||
buf.writeVarInt(xpLevel);
|
||||
buf.writeVarInt(totalExperience);
|
||||
buf.writeVarLong(familyMinerais);
|
||||
buf.writeVarLong(familyBois);
|
||||
buf.writeVarLong(familyRoche);
|
||||
buf.writeVarLong(familySols);
|
||||
buf.writeVarLong(familyVegetal);
|
||||
buf.writeVarLong(familyConstruits);
|
||||
buf.writeVarLong(familyAutres);
|
||||
}
|
||||
|
||||
private static long customStat(ServerStatsCounter stats, Identifier id) {
|
||||
return stats.getValue(Stats.CUSTOM.get(id));
|
||||
}
|
||||
|
||||
private static long distanceCentimeters(ServerStatsCounter stats) {
|
||||
return customStat(stats, Stats.WALK_ONE_CM)
|
||||
+ customStat(stats, Stats.CROUCH_ONE_CM)
|
||||
+ customStat(stats, Stats.SPRINT_ONE_CM)
|
||||
+ customStat(stats, Stats.WALK_ON_WATER_ONE_CM)
|
||||
+ customStat(stats, Stats.WALK_UNDER_WATER_ONE_CM)
|
||||
+ customStat(stats, Stats.SWIM_ONE_CM)
|
||||
+ customStat(stats, Stats.FLY_ONE_CM)
|
||||
+ customStat(stats, Stats.AVIATE_ONE_CM)
|
||||
+ customStat(stats, Stats.BOAT_ONE_CM)
|
||||
+ customStat(stats, Stats.MINECART_ONE_CM)
|
||||
+ customStat(stats, Stats.HORSE_ONE_CM);
|
||||
}
|
||||
|
||||
private static long minedBlocks(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
total += stats.getValue(Stats.BLOCK_MINED, block);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long craftedItems(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
total += stats.getValue(Stats.ITEM_CRAFTED, item);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long pickedBlockItems(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item instanceof BlockItem) {
|
||||
total += stats.getValue(Stats.ITEM_PICKED_UP, item);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long usedItems(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
total += stats.getValue(Stats.ITEM_USED, item);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long droppedItems(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
total += stats.getValue(Stats.ITEM_DROPPED, item);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long brokenItems(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
total += stats.getValue(Stats.ITEM_BROKEN, item);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long usedBlockItems(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item instanceof BlockItem) {
|
||||
total += stats.getValue(Stats.ITEM_USED, item);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static long killedByMobs(ServerStatsCounter stats) {
|
||||
long total = 0L;
|
||||
for (EntityType<?> entityType : BuiltInRegistries.ENTITY_TYPE) {
|
||||
total += stats.getValue(Stats.ENTITY_KILLED_BY, entityType);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static FamilyCounts pickedBlockFamilies(ServerStatsCounter stats) {
|
||||
FamilyCounts counts = new FamilyCounts();
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item instanceof BlockItem blockItem) {
|
||||
long count = stats.getValue(Stats.ITEM_PICKED_UP, item);
|
||||
if (count > 0L) {
|
||||
counts.add(blockFamily(blockItem.getBlock()), count);
|
||||
}
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
private static String blockFamily(Block block) {
|
||||
Identifier id = BuiltInRegistries.BLOCK.getKey(block);
|
||||
String path = id == null ? "" : id.getPath().toLowerCase(Locale.ROOT);
|
||||
if (path.contains("ore") || path.contains("raw_") || path.contains("coal") || path.contains("copper") || path.contains("iron") || path.contains("gold") || path.contains("diamond") || path.contains("emerald") || path.contains("lapis") || path.contains("redstone") || path.contains("quartz") || path.contains("ancient_debris")) {
|
||||
return "Minerais";
|
||||
}
|
||||
if (path.contains("log") || path.contains("wood") || path.contains("planks") || path.contains("stem") || path.contains("hyphae") || path.contains("bamboo")) {
|
||||
return "Bois";
|
||||
}
|
||||
if (path.contains("stone") || path.contains("deepslate") || path.contains("granite") || path.contains("andesite") || path.contains("diorite") || path.contains("tuff") || path.contains("basalt") || path.contains("blackstone") || path.contains("netherrack")) {
|
||||
return "Roche";
|
||||
}
|
||||
if (path.contains("dirt") || path.contains("grass_block") || path.contains("sand") || path.contains("gravel") || path.contains("clay") || path.contains("mud") || path.contains("soul")) {
|
||||
return "Sols";
|
||||
}
|
||||
if (path.contains("leaves") || path.contains("sapling") || path.contains("flower") || path.contains("moss") || path.contains("grass") || path.contains("vine") || path.contains("roots") || path.contains("crop")) {
|
||||
return "Vegetal";
|
||||
}
|
||||
if (path.contains("glass") || path.contains("wool") || path.contains("concrete") || path.contains("terracotta") || path.contains("brick")) {
|
||||
return "Construits";
|
||||
}
|
||||
return "Autres";
|
||||
}
|
||||
|
||||
private static final class FamilyCounts {
|
||||
private long minerais;
|
||||
private long bois;
|
||||
private long roche;
|
||||
private long sols;
|
||||
private long vegetal;
|
||||
private long construits;
|
||||
private long autres;
|
||||
|
||||
private void add(String family, long amount) {
|
||||
switch (family) {
|
||||
case "Minerais" -> minerais += amount;
|
||||
case "Bois" -> bois += amount;
|
||||
case "Roche" -> roche += amount;
|
||||
case "Sols" -> sols += amount;
|
||||
case "Vegetal" -> vegetal += amount;
|
||||
case "Construits" -> construits += amount;
|
||||
default -> autres += amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka99cab.computer.network;
|
||||
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
public record ComputerStatsRequestPayload() implements CustomPacketPayload {
|
||||
public static final Type<ComputerStatsRequestPayload> TYPE = new Type<>(ComputerMod.id("stats_request"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerStatsRequestPayload> STREAM_CODEC = StreamCodec.unit(new ComputerStatsRequestPayload());
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fr.koka99cab.computer.registry;
|
||||
|
||||
import fr.koka99cab.computer.ComputerMod;
|
||||
import fr.koka99cab.computer.block.ComputerBlock;
|
||||
import java.util.function.Function;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
|
||||
public final class ComputerBlocks {
|
||||
public static final Block COMPUTER = register(
|
||||
"computer",
|
||||
ComputerBlock::new,
|
||||
BlockBehaviour.Properties.of().strength(2.0F, 6.0F).sound(SoundType.METAL),
|
||||
true
|
||||
);
|
||||
|
||||
private ComputerBlocks() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
CreativeModeTabEvents.modifyOutputEvent(vanillaTab("functional_blocks")).register(entries ->
|
||||
entries.accept(new ItemStack(COMPUTER), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS)
|
||||
);
|
||||
}
|
||||
|
||||
private static Block register(String path, Function<BlockBehaviour.Properties, Block> factory, BlockBehaviour.Properties properties, boolean registerItem) {
|
||||
ResourceKey<Block> blockKey = ResourceKey.create(Registries.BLOCK, ComputerMod.id(path));
|
||||
Block block = factory.apply(properties.setId(blockKey));
|
||||
|
||||
if (registerItem) {
|
||||
ResourceKey<Item> itemKey = ResourceKey.create(Registries.ITEM, ComputerMod.id(path));
|
||||
BlockItem item = new BlockItem(block, new Item.Properties().setId(itemKey).useBlockDescriptionPrefix());
|
||||
Registry.register(BuiltInRegistries.ITEM, itemKey, item);
|
||||
}
|
||||
|
||||
return Registry.register(BuiltInRegistries.BLOCK, blockKey, block);
|
||||
}
|
||||
|
||||
private static ResourceKey<CreativeModeTab> vanillaTab(String path) {
|
||||
return ResourceKey.create(Registries.CREATIVE_MODE_TAB, Identifier.fromNamespaceAndPath("minecraft", path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package fr.koka99cab.computer.voxel;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ComputerVoxelEditor {
|
||||
public static final int DEFAULT_WIDTH = 16;
|
||||
public static final int DEFAULT_HEIGHT = 16;
|
||||
public static final int DEFAULT_DEPTH = 16;
|
||||
private static final int SHAPE_TOOL_RADIUS = 2;
|
||||
|
||||
public enum Tool {
|
||||
PIPETTE,
|
||||
BRUSH,
|
||||
ADD,
|
||||
REMOVE,
|
||||
SQUARE,
|
||||
ROUND,
|
||||
FILL;
|
||||
|
||||
public String label() {
|
||||
return switch (this) {
|
||||
case PIPETTE -> "Pipette";
|
||||
case BRUSH -> "Pinceau";
|
||||
case ADD -> "Ajouter";
|
||||
case REMOVE -> "Retirer";
|
||||
case SQUARE -> "Carre";
|
||||
case ROUND -> "Rond";
|
||||
case FILL -> "Remplir";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private ComputerVoxelProject project;
|
||||
private String selectedBlockId;
|
||||
|
||||
private ComputerVoxelEditor(ComputerVoxelProject project, String selectedBlockId) {
|
||||
this.project = normalize(project);
|
||||
this.selectedBlockId = cleanBlockId(selectedBlockId);
|
||||
}
|
||||
|
||||
public static ComputerVoxelEditor blank(String id, String name) {
|
||||
return blank(id, name, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_DEPTH);
|
||||
}
|
||||
|
||||
public static ComputerVoxelEditor blank(String id, String name, int width, int height, int depth) {
|
||||
requirePositiveDimensions(width, height, depth);
|
||||
return new ComputerVoxelEditor(new ComputerVoxelProject(id, name, width, height, depth, List.of(), Map.of(), System.currentTimeMillis(), System.currentTimeMillis()), null);
|
||||
}
|
||||
|
||||
public static ComputerVoxelEditor fromProject(ComputerVoxelProject project) {
|
||||
return new ComputerVoxelEditor(project, null);
|
||||
}
|
||||
|
||||
public ComputerVoxelProject project() {
|
||||
return project;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return project.id();
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return project.name();
|
||||
}
|
||||
|
||||
public int width() {
|
||||
return project.width();
|
||||
}
|
||||
|
||||
public int height() {
|
||||
return project.height();
|
||||
}
|
||||
|
||||
public int depth() {
|
||||
return project.depth();
|
||||
}
|
||||
|
||||
public Map<String, String> cells() {
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(project.cells()));
|
||||
}
|
||||
|
||||
public String selectedBlockId() {
|
||||
return selectedBlockId;
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor selectBlock(String blockId) {
|
||||
selectedBlockId = cleanBlockId(blockId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor setName(String name) {
|
||||
project = new ComputerVoxelProject(project.id(), name, project.width(), project.height(), project.depth(), project.palette(), project.cells(), project.createdAt(), project.updatedAt());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor replaceProject(ComputerVoxelProject project) {
|
||||
this.project = normalize(project);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor resize(int width, int height, int depth) {
|
||||
requirePositiveDimensions(width, height, depth);
|
||||
Map<String, String> cells = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : project.cells().entrySet()) {
|
||||
CellEntry pos = parseCellKey(entry.getKey(), entry.getValue());
|
||||
if (pos != null && inBounds(pos.x(), pos.y(), pos.z(), width, height, depth)) {
|
||||
cells.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return withProject(width, height, depth, cells);
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor add(int x, int y, int z, String blockId) {
|
||||
String cleanBlockId = requireBlockId(blockId);
|
||||
if (!inProjectBounds(x, y, z)) {
|
||||
return this;
|
||||
}
|
||||
Map<String, String> cells = mutableCells();
|
||||
cells.put(cellKey(x, y, z), cleanBlockId);
|
||||
return withCells(cells);
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor remove(int x, int y, int z) {
|
||||
if (!inProjectBounds(x, y, z)) {
|
||||
return this;
|
||||
}
|
||||
Map<String, String> cells = mutableCells();
|
||||
cells.remove(cellKey(x, y, z));
|
||||
return withCells(cells);
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor fillSquare(int centerX, int y, int centerZ, int radius, String blockId) {
|
||||
String cleanBlockId = requireBlockId(blockId);
|
||||
if (y < 0 || y >= project.height()) {
|
||||
return this;
|
||||
}
|
||||
int startX = Math.max(0, centerX - radius);
|
||||
int endX = Math.min(project.width() - 1, centerX + radius);
|
||||
int startZ = Math.max(0, centerZ - radius);
|
||||
int endZ = Math.min(project.depth() - 1, centerZ + radius);
|
||||
Map<String, String> cells = mutableCells();
|
||||
for (int z = startZ; z <= endZ; z++) {
|
||||
for (int x = startX; x <= endX; x++) {
|
||||
cells.put(cellKey(x, y, z), cleanBlockId);
|
||||
}
|
||||
}
|
||||
return withCells(cells);
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor fillRound(int centerX, int y, int centerZ, int radius, String blockId) {
|
||||
String cleanBlockId = requireBlockId(blockId);
|
||||
if (radius < 0 || y < 0 || y >= project.height()) {
|
||||
return this;
|
||||
}
|
||||
int startX = Math.max(0, centerX - radius);
|
||||
int endX = Math.min(project.width() - 1, centerX + radius);
|
||||
int startZ = Math.max(0, centerZ - radius);
|
||||
int endZ = Math.min(project.depth() - 1, centerZ + radius);
|
||||
int radiusSquared = radius * radius;
|
||||
Map<String, String> cells = mutableCells();
|
||||
for (int z = startZ; z <= endZ; z++) {
|
||||
int dz = z - centerZ;
|
||||
for (int x = startX; x <= endX; x++) {
|
||||
int dx = x - centerX;
|
||||
if (dx * dx + dz * dz <= radiusSquared) {
|
||||
cells.put(cellKey(x, y, z), cleanBlockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return withCells(cells);
|
||||
}
|
||||
|
||||
public ComputerVoxelEditor floodFill2d(int startX, int y, int startZ, String blockId) {
|
||||
String cleanBlockId = requireBlockId(blockId);
|
||||
if (!inProjectBounds(startX, y, startZ)) {
|
||||
return this;
|
||||
}
|
||||
String targetBlockId = project.cells().get(cellKey(startX, y, startZ));
|
||||
if (Objects.equals(targetBlockId, cleanBlockId)) {
|
||||
return this;
|
||||
}
|
||||
Map<String, String> cells = mutableCells();
|
||||
Queue<LayerPos> queue = new ArrayDeque<>();
|
||||
Set<LayerPos> visited = new HashSet<>();
|
||||
queue.add(new LayerPos(startX, startZ));
|
||||
while (!queue.isEmpty()) {
|
||||
LayerPos current = queue.remove();
|
||||
if (!visited.add(current) || !inProjectBounds(current.x(), y, current.z())) {
|
||||
continue;
|
||||
}
|
||||
String currentKey = cellKey(current.x(), y, current.z());
|
||||
if (!Objects.equals(cells.get(currentKey), targetBlockId)) {
|
||||
continue;
|
||||
}
|
||||
cells.put(currentKey, cleanBlockId);
|
||||
queue.add(new LayerPos(current.x() + 1, current.z()));
|
||||
queue.add(new LayerPos(current.x() - 1, current.z()));
|
||||
queue.add(new LayerPos(current.x(), current.z() + 1));
|
||||
queue.add(new LayerPos(current.x(), current.z() - 1));
|
||||
}
|
||||
return withCells(cells);
|
||||
}
|
||||
|
||||
public String blockAt(int x, int y, int z) {
|
||||
if (!inProjectBounds(x, y, z)) {
|
||||
return null;
|
||||
}
|
||||
return project.cells().get(cellKey(x, y, z));
|
||||
}
|
||||
|
||||
public void apply(Tool tool, int x, int y, int z, String blockId) {
|
||||
switch (tool) {
|
||||
case PIPETTE -> {
|
||||
}
|
||||
case BRUSH, ADD -> add(x, y, z, blockId);
|
||||
case REMOVE -> remove(x, y, z);
|
||||
case SQUARE -> fillSquare(x, y, z, SHAPE_TOOL_RADIUS, blockId);
|
||||
case ROUND -> fillRound(x, y, z, SHAPE_TOOL_RADIUS, blockId);
|
||||
case FILL -> floodFill2d(x, y, z, blockId);
|
||||
}
|
||||
}
|
||||
|
||||
public ComputerVoxelProject toProject(long updatedAt) {
|
||||
ComputerVoxelProject normalized = normalize(project);
|
||||
return new ComputerVoxelProject(normalized.id(), normalized.name(), normalized.width(), normalized.height(), normalized.depth(), normalized.palette(), normalized.cells(), normalized.createdAt(), updatedAt);
|
||||
}
|
||||
|
||||
public ComputerVoxelProject export() {
|
||||
return normalize(project);
|
||||
}
|
||||
|
||||
private ComputerVoxelEditor withCells(Map<String, String> cells) {
|
||||
return withProject(project.width(), project.height(), project.depth(), cells);
|
||||
}
|
||||
|
||||
private ComputerVoxelEditor withProject(int width, int height, int depth, Map<String, String> cells) {
|
||||
project = normalize(new ComputerVoxelProject(project.id(), project.name(), width, height, depth, paletteFromCells(cells), cells, project.createdAt(), System.currentTimeMillis()));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Map<String, String> mutableCells() {
|
||||
return new LinkedHashMap<>(project.cells());
|
||||
}
|
||||
|
||||
private boolean inProjectBounds(int x, int y, int z) {
|
||||
return inBounds(x, y, z, project.width(), project.height(), project.depth());
|
||||
}
|
||||
|
||||
private static ComputerVoxelProject normalize(ComputerVoxelProject project) {
|
||||
Objects.requireNonNull(project, "project");
|
||||
requirePositiveDimensions(project.width(), project.height(), project.depth());
|
||||
Map<String, String> cells = new LinkedHashMap<>();
|
||||
normalizedCellEntries(project).forEach(entry -> cells.put(entry.key(), entry.blockId()));
|
||||
return new ComputerVoxelProject(project.id(), project.name(), project.width(), project.height(), project.depth(), paletteFromCells(cells), cells, project.createdAt(), project.updatedAt());
|
||||
}
|
||||
|
||||
private static List<CellEntry> normalizedCellEntries(ComputerVoxelProject project) {
|
||||
List<CellEntry> entries = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : project.cells().entrySet()) {
|
||||
CellEntry pos = parseCellKey(entry.getKey(), entry.getValue());
|
||||
if (pos != null && inBounds(pos.x(), pos.y(), pos.z(), project.width(), project.height(), project.depth())) {
|
||||
entries.add(pos);
|
||||
}
|
||||
}
|
||||
entries.sort(Comparator.comparingInt(CellEntry::y).thenComparingInt(CellEntry::z).thenComparingInt(CellEntry::x));
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<String> paletteFromCells(Map<String, String> cells) {
|
||||
List<String> palette = new ArrayList<>();
|
||||
for (String blockId : cells.values()) {
|
||||
if (!palette.contains(blockId)) {
|
||||
palette.add(blockId);
|
||||
}
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
private static boolean inBounds(int x, int y, int z, int width, int height, int depth) {
|
||||
return x >= 0 && x < width && y >= 0 && y < height && z >= 0 && z < depth;
|
||||
}
|
||||
|
||||
private static CellEntry parseCellKey(String key, String blockId) {
|
||||
String cleanBlockId = cleanBlockId(blockId);
|
||||
if (key == null || cleanBlockId == null) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = key.split(",", 3);
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new CellEntry(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]), cleanBlockId);
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String cellKey(int x, int y, int z) {
|
||||
return x + "," + y + "," + z;
|
||||
}
|
||||
|
||||
private static void requirePositiveDimensions(int width, int height, int depth) {
|
||||
if (width <= 0 || height <= 0 || depth <= 0 || width > ComputerVoxelValidator.MAX_DIMENSION || height > ComputerVoxelValidator.MAX_DIMENSION || depth > ComputerVoxelValidator.MAX_DIMENSION) {
|
||||
throw new IllegalArgumentException("Voxel project dimensions must be between 1 and " + ComputerVoxelValidator.MAX_DIMENSION);
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireBlockId(String blockId) {
|
||||
String cleanBlockId = cleanBlockId(blockId);
|
||||
if (cleanBlockId == null) {
|
||||
throw new IllegalStateException("A block must be selected before painting");
|
||||
}
|
||||
return cleanBlockId;
|
||||
}
|
||||
|
||||
private static String cleanBlockId(String blockId) {
|
||||
if (blockId == null || blockId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return blockId.trim();
|
||||
}
|
||||
|
||||
private record LayerPos(int x, int z) {
|
||||
}
|
||||
|
||||
private record CellEntry(int x, int y, int z, String blockId) {
|
||||
String key() {
|
||||
return cellKey(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package fr.koka99cab.computer.voxel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
public final class ComputerVoxelPlacement {
|
||||
private ComputerVoxelPlacement() {
|
||||
}
|
||||
|
||||
public static List<Cell> orderedCells(ComputerVoxelProject project) {
|
||||
List<Cell> cells = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : project.cells().entrySet()) {
|
||||
BlockPos pos = parseCell(entry.getKey());
|
||||
if (pos != null) {
|
||||
cells.add(new Cell(pos.getX(), pos.getY(), pos.getZ(), entry.getValue()));
|
||||
}
|
||||
}
|
||||
cells.sort(Comparator.comparingInt(Cell::y).thenComparingInt(Cell::z).thenComparingInt(Cell::x));
|
||||
return cells;
|
||||
}
|
||||
|
||||
public static BlockPos transform(ComputerVoxelProject project, Cell cell, BlockPos anchor, Direction facing) {
|
||||
Direction horizontal = horizontal(facing);
|
||||
int centeredX = cell.x() - project.width() / 2;
|
||||
int centeredZ = cell.z() - project.depth() / 2;
|
||||
int worldX;
|
||||
int worldZ;
|
||||
switch (horizontal) {
|
||||
case SOUTH -> {
|
||||
worldX = -centeredX;
|
||||
worldZ = -centeredZ;
|
||||
}
|
||||
case EAST -> {
|
||||
worldX = -centeredZ;
|
||||
worldZ = centeredX;
|
||||
}
|
||||
case WEST -> {
|
||||
worldX = centeredZ;
|
||||
worldZ = -centeredX;
|
||||
}
|
||||
default -> {
|
||||
worldX = centeredX;
|
||||
worldZ = centeredZ;
|
||||
}
|
||||
}
|
||||
return anchor.offset(worldX, cell.y(), worldZ).immutable();
|
||||
}
|
||||
|
||||
public static BlockPos parseCell(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = key.split(",", 3);
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BlockPos(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String cellKey(int x, int y, int z) {
|
||||
return x + "," + y + "," + z;
|
||||
}
|
||||
|
||||
public static Direction horizontal(Direction facing) {
|
||||
if (facing == Direction.UP || facing == Direction.DOWN || facing == null) {
|
||||
return Direction.NORTH;
|
||||
}
|
||||
return facing;
|
||||
}
|
||||
|
||||
public record Cell(int x, int y, int z, String blockId) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package fr.koka99cab.computer.voxel;
|
||||
|
||||
import fr.koka99cab.computer.network.ComputerPlaceVoxelProjectPayload;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class ComputerVoxelPlacementHandler {
|
||||
private static final int MAX_PLACEMENT_HISTORY = 8;
|
||||
private static final Map<UUID, Deque<PlacementSnapshot>> UNDO_HISTORY = new HashMap<>();
|
||||
private static final Map<UUID, Deque<PlacementSnapshot>> REDO_HISTORY = new HashMap<>();
|
||||
|
||||
private ComputerVoxelPlacementHandler() {
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, ComputerPlaceVoxelProjectPayload payload) {
|
||||
if (ComputerPlaceVoxelProjectPayload.MODE_UNDO.equals(payload.mode())) {
|
||||
undoPlacement(player);
|
||||
return;
|
||||
}
|
||||
if (ComputerPlaceVoxelProjectPayload.MODE_REDO.equals(payload.mode())) {
|
||||
redoPlacement(player);
|
||||
return;
|
||||
}
|
||||
if (!ComputerPlaceVoxelProjectPayload.MODE_CREATIVE_DIRECT.equals(payload.mode())) {
|
||||
player.sendSystemMessage(Component.literal("Mode de pose inconnu.").withStyle(ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
if (!player.isCreative()) {
|
||||
player.sendSystemMessage(Component.literal("Pose directe reservee au mode creatif.").withStyle(ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
ComputerVoxelValidator.ValidationResult validation = ComputerVoxelValidator.validate(payload.project());
|
||||
if (!validation.valid()) {
|
||||
player.sendSystemMessage(Component.literal(validation.message()).withStyle(ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
int placed = 0;
|
||||
List<BlockChange> changes = new ArrayList<>();
|
||||
for (ComputerVoxelPlacement.Cell cell : ComputerVoxelPlacement.orderedCells(payload.project())) {
|
||||
Optional<Block> block = block(cell.blockId());
|
||||
if (block.isEmpty() || block.get() == Blocks.AIR) {
|
||||
continue;
|
||||
}
|
||||
BlockPos worldPos = ComputerVoxelPlacement.transform(payload.project(), cell, payload.anchor(), payload.facing());
|
||||
BlockState before = player.level().getBlockState(worldPos);
|
||||
BlockState after = block.get().defaultBlockState();
|
||||
if (before.equals(after)) {
|
||||
continue;
|
||||
}
|
||||
player.level().setBlock(worldPos, after, 3);
|
||||
changes.add(new BlockChange(worldPos.immutable(), before, after));
|
||||
placed++;
|
||||
}
|
||||
if (!changes.isEmpty()) {
|
||||
pushUndo(player.getUUID(), new PlacementSnapshot(List.copyOf(changes)));
|
||||
REDO_HISTORY.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>()).clear();
|
||||
}
|
||||
player.sendSystemMessage(Component.literal("Structure posee: " + placed + " blocs.").withStyle(ChatFormatting.GREEN));
|
||||
}
|
||||
|
||||
private static void undoPlacement(ServerPlayer player) {
|
||||
Deque<PlacementSnapshot> undo = UNDO_HISTORY.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>());
|
||||
PlacementSnapshot snapshot = undo.pollFirst();
|
||||
if (snapshot == null) {
|
||||
player.sendSystemMessage(Component.literal("Aucune construction a annuler.").withStyle(ChatFormatting.YELLOW));
|
||||
return;
|
||||
}
|
||||
restore(player, snapshot, false);
|
||||
REDO_HISTORY.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>()).push(snapshot);
|
||||
player.sendSystemMessage(Component.literal("Construction annulee.").withStyle(ChatFormatting.YELLOW));
|
||||
}
|
||||
|
||||
private static void redoPlacement(ServerPlayer player) {
|
||||
Deque<PlacementSnapshot> redo = REDO_HISTORY.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>());
|
||||
PlacementSnapshot snapshot = redo.pollFirst();
|
||||
if (snapshot == null) {
|
||||
player.sendSystemMessage(Component.literal("Aucune construction a retablir.").withStyle(ChatFormatting.YELLOW));
|
||||
return;
|
||||
}
|
||||
restore(player, snapshot, true);
|
||||
pushUndo(player.getUUID(), snapshot);
|
||||
player.sendSystemMessage(Component.literal("Construction retablie.").withStyle(ChatFormatting.GREEN));
|
||||
}
|
||||
|
||||
private static void restore(ServerPlayer player, PlacementSnapshot snapshot, boolean after) {
|
||||
List<BlockChange> changes = snapshot.changes();
|
||||
if (after) {
|
||||
for (BlockChange change : changes) {
|
||||
player.level().setBlock(change.pos(), change.after(), 3);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int index = changes.size() - 1; index >= 0; index--) {
|
||||
BlockChange change = changes.get(index);
|
||||
player.level().setBlock(change.pos(), change.before(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pushUndo(UUID playerId, PlacementSnapshot snapshot) {
|
||||
Deque<PlacementSnapshot> undo = UNDO_HISTORY.computeIfAbsent(playerId, ignored -> new ArrayDeque<>());
|
||||
undo.push(snapshot);
|
||||
while (undo.size() > MAX_PLACEMENT_HISTORY) {
|
||||
undo.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<Block> block(String blockId) {
|
||||
Identifier id = Identifier.tryParse(blockId == null ? "" : blockId);
|
||||
return id == null ? Optional.empty() : BuiltInRegistries.BLOCK.getOptional(id);
|
||||
}
|
||||
|
||||
private record BlockChange(BlockPos pos, BlockState before, BlockState after) {
|
||||
}
|
||||
|
||||
private record PlacementSnapshot(List<BlockChange> changes) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.koka99cab.computer.voxel;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record ComputerVoxelProject(
|
||||
String id,
|
||||
String name,
|
||||
int width,
|
||||
int height,
|
||||
int depth,
|
||||
List<String> palette,
|
||||
Map<String, String> cells,
|
||||
long createdAt,
|
||||
long updatedAt
|
||||
) {
|
||||
public static final Codec<ComputerVoxelProject> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("id").forGetter(ComputerVoxelProject::id),
|
||||
Codec.STRING.optionalFieldOf("name", "Untitled").forGetter(ComputerVoxelProject::name),
|
||||
Codec.INT.optionalFieldOf("width", 0).forGetter(ComputerVoxelProject::width),
|
||||
Codec.INT.optionalFieldOf("height", 0).forGetter(ComputerVoxelProject::height),
|
||||
Codec.INT.optionalFieldOf("depth", 0).forGetter(ComputerVoxelProject::depth),
|
||||
Codec.STRING.listOf().optionalFieldOf("palette", List.of()).forGetter(ComputerVoxelProject::palette),
|
||||
Codec.unboundedMap(Codec.STRING, Codec.STRING).optionalFieldOf("cells", Map.of()).forGetter(ComputerVoxelProject::cells),
|
||||
Codec.LONG.optionalFieldOf("created_at", 0L).forGetter(ComputerVoxelProject::createdAt),
|
||||
Codec.LONG.optionalFieldOf("updated_at", 0L).forGetter(ComputerVoxelProject::updatedAt)
|
||||
).apply(instance, ComputerVoxelProject::new));
|
||||
|
||||
public ComputerVoxelProject {
|
||||
id = id == null || id.isBlank() ? "voxel-project" : id.strip();
|
||||
name = name == null || name.isBlank() ? "Untitled" : name.strip();
|
||||
palette = List.copyOf(palette);
|
||||
cells = new LinkedHashMap<>(cells);
|
||||
}
|
||||
|
||||
public static ComputerVoxelProject empty() {
|
||||
return new ComputerVoxelProject("empty", "Empty", 1, 1, 1, List.of(), Map.of(), 0L, 0L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package fr.koka99cab.computer.voxel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ComputerVoxelRequirements {
|
||||
private ComputerVoxelRequirements() {
|
||||
}
|
||||
|
||||
public static Map<String, Integer> requiredBlockCounts(ComputerVoxelProject project) {
|
||||
Map<String, Integer> counts = new LinkedHashMap<>();
|
||||
if (project == null) {
|
||||
return counts;
|
||||
}
|
||||
for (String blockId : project.cells().values()) {
|
||||
if (blockId != null && !blockId.isBlank()) {
|
||||
counts.merge(blockId, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
return sortedByCount(counts);
|
||||
}
|
||||
|
||||
public static String formatCounts(Map<String, Integer> counts, int limit) {
|
||||
if (counts.isEmpty()) {
|
||||
return "aucun";
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
|
||||
if (index >= limit) {
|
||||
parts.add("+" + (counts.size() - limit));
|
||||
break;
|
||||
}
|
||||
parts.add(shortId(entry.getKey()) + " x" + entry.getValue());
|
||||
index++;
|
||||
}
|
||||
return String.join(", ", parts);
|
||||
}
|
||||
|
||||
public static String shortId(String id) {
|
||||
int split = id.indexOf(':');
|
||||
return split >= 0 ? id.substring(split + 1) : id;
|
||||
}
|
||||
|
||||
private static Map<String, Integer> sortedByCount(Map<String, Integer> counts) {
|
||||
Map<String, Integer> sorted = new LinkedHashMap<>();
|
||||
counts.entrySet().stream()
|
||||
.sorted(Comparator.<Map.Entry<String, Integer>>comparingInt(Map.Entry::getValue).reversed().thenComparing(Map.Entry::getKey))
|
||||
.forEach(entry -> sorted.put(entry.getKey(), entry.getValue()));
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package fr.koka99cab.computer.voxel;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class ComputerVoxelValidator {
|
||||
public static final int MAX_DIMENSION = 32;
|
||||
public static final int MAX_CELLS = 8192;
|
||||
public static final int MAX_PALETTE = 1024;
|
||||
|
||||
private ComputerVoxelValidator() {
|
||||
}
|
||||
|
||||
public static ValidationResult validate(ComputerVoxelProject project) {
|
||||
if (project == null) {
|
||||
return ValidationResult.error("Projet absent.");
|
||||
}
|
||||
if (project.id() == null || project.id().isBlank()) {
|
||||
return ValidationResult.error("Identifiant de projet invalide.");
|
||||
}
|
||||
if (!validDimension(project.width()) || !validDimension(project.height()) || !validDimension(project.depth())) {
|
||||
return ValidationResult.error("Dimensions invalides. Maximum " + MAX_DIMENSION + " par axe.");
|
||||
}
|
||||
if (project.cells().size() > MAX_CELLS) {
|
||||
return ValidationResult.error("Structure trop grande. Maximum " + MAX_CELLS + " voxels.");
|
||||
}
|
||||
if (project.palette().size() > MAX_PALETTE) {
|
||||
return ValidationResult.error("Palette trop grande. Maximum " + MAX_PALETTE + " blocs.");
|
||||
}
|
||||
|
||||
Set<String> palette = new HashSet<>(project.palette());
|
||||
for (String blockId : palette) {
|
||||
if (!isBuildableFullBlock(blockId)) {
|
||||
return ValidationResult.error("Bloc non constructible dans la palette: " + blockId);
|
||||
}
|
||||
}
|
||||
|
||||
for (var entry : project.cells().entrySet()) {
|
||||
BlockPos cell = ComputerVoxelPlacement.parseCell(entry.getKey());
|
||||
if (cell == null || cell.getX() < 0 || cell.getY() < 0 || cell.getZ() < 0 || cell.getX() >= project.width() || cell.getY() >= project.height() || cell.getZ() >= project.depth()) {
|
||||
return ValidationResult.error("Cellule hors limites: " + entry.getKey());
|
||||
}
|
||||
String blockId = entry.getValue();
|
||||
if (!palette.contains(blockId)) {
|
||||
return ValidationResult.error("Cellule hors palette: " + blockId);
|
||||
}
|
||||
if (!isBuildableFullBlock(blockId)) {
|
||||
return ValidationResult.error("Bloc non constructible dans une cellule: " + blockId);
|
||||
}
|
||||
}
|
||||
return ValidationResult.ok();
|
||||
}
|
||||
|
||||
public static boolean isBuildableFullBlock(String blockId) {
|
||||
Identifier id = Identifier.tryParse(blockId);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
Block block = BuiltInRegistries.BLOCK.getOptional(id).orElse(Blocks.AIR);
|
||||
if (block == Blocks.AIR || block.asItem() == Items.AIR) {
|
||||
return false;
|
||||
}
|
||||
BlockState state = block.defaultBlockState();
|
||||
return !state.isAir() && state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
|
||||
}
|
||||
|
||||
public static boolean isOpaquePaletteBlock(String blockId) {
|
||||
if (!isBuildableFullBlock(blockId)) {
|
||||
return false;
|
||||
}
|
||||
Identifier id = Identifier.tryParse(blockId);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
BlockState state = BuiltInRegistries.BLOCK.getOptional(id).orElse(Blocks.AIR).defaultBlockState();
|
||||
return state.canOcclude() && state.isSolidRender();
|
||||
}
|
||||
|
||||
private static boolean validDimension(int value) {
|
||||
return value > 0 && value <= MAX_DIMENSION;
|
||||
}
|
||||
|
||||
public record ValidationResult(boolean valid, String message) {
|
||||
public static ValidationResult ok() {
|
||||
return new ValidationResult(true, "");
|
||||
}
|
||||
|
||||
public static ValidationResult error(String message) {
|
||||
return new ValidationResult(false, message == null ? "Projet invalide." : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=east": {
|
||||
"model": "computer:block/computer",
|
||||
"y": 90
|
||||
},
|
||||
"facing=north": {
|
||||
"model": "computer:block/computer"
|
||||
},
|
||||
"facing=south": {
|
||||
"model": "computer:block/computer",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"model": "computer:block/computer",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "computer:block/computer"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"block.computer.computer": "Computer",
|
||||
"itemGroup.computer": "Computer"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"block.computer.computer": "Computer",
|
||||
"itemGroup.computer": "Computer"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"parent": "minecraft:block/cube",
|
||||
"textures": {
|
||||
"down": "computer:block/computer_bottom",
|
||||
"east": "computer:block/computer_left",
|
||||
"north": "computer:block/computer_front",
|
||||
"particle": "computer:block/computer_front",
|
||||
"south": "computer:block/computer_back",
|
||||
"up": "computer:block/computer_top",
|
||||
"west": "computer:block/computer_right"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 191 B |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 269 B |
|
After Width: | Height: | Size: 179 B |
|
After Width: | Height: | Size: 240 B |
|
After Width: | Height: | Size: 104 B |
|
After Width: | Height: | Size: 110 B |
|
After Width: | Height: | Size: 107 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 137 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 414 B |
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 177 B |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 264 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 203 B |
|
After Width: | Height: | Size: 712 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 892 B |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 472 KiB |
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_redstone": {
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:redstone"
|
||||
}
|
||||
]
|
||||
},
|
||||
"trigger": "minecraft:inventory_changed"
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"conditions": {
|
||||
"recipe": "computer:computer"
|
||||
},
|
||||
"trigger": "minecraft:recipe_unlocked"
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_the_recipe",
|
||||
"has_redstone"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"computer:computer"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"bonus_rolls": 0.0,
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "minecraft:survives_explosion"
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "computer:computer"
|
||||
}
|
||||
],
|
||||
"rolls": 1.0
|
||||
}
|
||||
],
|
||||
"random_sequence": "computer:blocks/computer"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "redstone",
|
||||
"key": {
|
||||
"#": "minecraft:iron_ingot",
|
||||
"G": "minecraft:glass_pane",
|
||||
"R": "minecraft:redstone"
|
||||
},
|
||||
"pattern": [
|
||||
"###",
|
||||
"#G#",
|
||||
"#R#"
|
||||
],
|
||||
"result": {
|
||||
"id": "computer:computer"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "computer",
|
||||
"version": "${version}",
|
||||
"name": "Computer",
|
||||
"description": "A Sanctuary desktop computer block with a simulated OS, launchable programs, movable windows, wallpapers, and map tools.",
|
||||
"authors": [
|
||||
"KOKA99CAB"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"icon": "assets/computer/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"fr.koka99cab.computer.ComputerMod"
|
||||
],
|
||||
"client": [
|
||||
"fr.koka99cab.computer.client.ComputerClient"
|
||||
]
|
||||
},
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.2",
|
||||
"minecraft": "~26.1.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": "*"
|
||||
}
|
||||
}
|
||||