Initial sanctuary sources
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Gradle
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
classes/
|
||||
|
||||
# Eclipse
|
||||
*.launch
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# VS Code
|
||||
.settings/
|
||||
.vscode/
|
||||
bin/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# macOS
|
||||
*.DS_Store
|
||||
|
||||
# Fabric
|
||||
run/
|
||||
|
||||
# Java
|
||||
.jdk/
|
||||
hs_err_*.log
|
||||
replay_*.log
|
||||
*.hprof
|
||||
*.jfr
|
||||
@@ -0,0 +1,80 @@
|
||||
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 {
|
||||
"canon" {
|
||||
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}"
|
||||
|
||||
testImplementation platform("org.junit:junit-bom:5.10.2")
|
||||
testImplementation "org.junit.jupiter:junit-jupiter"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
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,21 @@
|
||||
# Done to increase the memory available to Gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.parallel=true
|
||||
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache:
|
||||
# https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Fabric properties
|
||||
# Check current versions on https://fabricmc.net/develop
|
||||
minecraft_version=26.1.2
|
||||
loader_version=0.19.2
|
||||
loom_version=1.16-SNAPSHOT
|
||||
|
||||
# Mod properties
|
||||
mod_version=1.0.0
|
||||
maven_group=fr.koka99cab
|
||||
archives_base_name=canon
|
||||
|
||||
# Dependencies
|
||||
fabric_api_version=0.148.0+26.1.2
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+253
@@ -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" "$@"
|
||||
Vendored
+93
@@ -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 = "canon"
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka99cab.canon.client;
|
||||
|
||||
import fr.koka99cab.canon.client.hud.CameraViewfinderHud;
|
||||
import fr.koka99cab.canon.client.item.FilmTintSource;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
|
||||
public final class CanonClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
FilmTintSource.register();
|
||||
CameraViewfinderHud.register();
|
||||
CanonClientBridge.setCaptureHandler(ClientPhotoCapture::queueCapture);
|
||||
LevelRenderEvents.END_MAIN.register(context -> ClientPhotoCapture.capturePending());
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package fr.koka99cab.canon.client;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.canon.client.hud.CameraViewfinderLayout;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import fr.koka99cab.canon.network.CapturePhotoPayload;
|
||||
import fr.koka99cab.canon.photo.PhotoMapPalette;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.blaze3d.platform.Window;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class ClientPhotoCapture {
|
||||
private static PendingCapture pendingCapture;
|
||||
|
||||
private ClientPhotoCapture() {
|
||||
}
|
||||
|
||||
public static void queueCapture(ItemStack cameraStack, PhotoStyle style) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (client.player == null || client.level == null || !ClientPlayNetworking.canSend(CapturePhotoPayload.ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingCapture = new PendingCapture(cameraStack, style);
|
||||
}
|
||||
|
||||
public static void capturePending() {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
PendingCapture capture = pendingCapture;
|
||||
if (capture == null || client.player == null || client.level == null || !ClientPlayNetworking.canSend(CapturePhotoPayload.ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingCapture = null;
|
||||
Screenshot.takeScreenshot(client.getMainRenderTarget(), screenshot -> {
|
||||
try (screenshot) {
|
||||
Window window = client.getWindow();
|
||||
CameraViewfinderLayout.Square square = CameraViewfinderLayout.forWindow(window.getGuiScaledWidth(), window.getGuiScaledHeight());
|
||||
double scaleX = screenshot.getWidth() / (double)window.getGuiScaledWidth();
|
||||
double scaleY = screenshot.getHeight() / (double)window.getGuiScaledHeight();
|
||||
int offsetX = clamp(Math.round((float)(square.left() * scaleX)), 0, screenshot.getWidth() - 1);
|
||||
int offsetY = clamp(Math.round((float)(square.top() * scaleY)), 0, screenshot.getHeight() - 1);
|
||||
int right = clamp(Math.round((float)(square.right() * scaleX)), offsetX + 1, screenshot.getWidth());
|
||||
int bottom = clamp(Math.round((float)(square.bottom() * scaleY)), offsetY + 1, screenshot.getHeight());
|
||||
int cropWidth = right - offsetX;
|
||||
int cropHeight = bottom - offsetY;
|
||||
byte[] colors = new byte[CanonConstants.PHOTO_PIXEL_COUNT];
|
||||
|
||||
for (int y = 0; y < CanonConstants.MAP_SIZE; y++) {
|
||||
for (int x = 0; x < CanonConstants.MAP_SIZE; x++) {
|
||||
float sampleX = offsetX + ((x + 0.5F) * cropWidth / CanonConstants.MAP_SIZE) - 0.5F;
|
||||
float sampleY = offsetY + ((y + 0.5F) * cropHeight / CanonConstants.MAP_SIZE) - 0.5F;
|
||||
int argb = capture.style().applyFilter(sampleBilinearArgb(screenshot, sampleX, sampleY));
|
||||
colors[x + y * CanonConstants.MAP_SIZE] = PhotoMapPalette.nearestColor(argb);
|
||||
}
|
||||
}
|
||||
|
||||
ClientPlayNetworking.send(new CapturePhotoPayload(colors));
|
||||
client.player.getCooldowns().addCooldown(capture.cameraStack(), CanonConstants.CAMERA_COOLDOWN_TICKS);
|
||||
client.player.playSound(SoundEvents.SPYGLASS_USE, 0.7F, 1.35F);
|
||||
} catch (Exception exception) {
|
||||
CanonMod.LOGGER.warn("Failed to capture photo", exception);
|
||||
if (client.player != null) {
|
||||
client.player.sendOverlayMessage(Component.translatable("message.canon.camera.capture_failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int clamp(int value, int min, int max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private static int sampleBilinearArgb(NativeImage image, float x, float y) {
|
||||
int x0 = clamp((int)Math.floor(x), 0, image.getWidth() - 1);
|
||||
int y0 = clamp((int)Math.floor(y), 0, image.getHeight() - 1);
|
||||
int x1 = clamp(x0 + 1, 0, image.getWidth() - 1);
|
||||
int y1 = clamp(y0 + 1, 0, image.getHeight() - 1);
|
||||
float tx = clamp01(x - x0);
|
||||
float ty = clamp01(y - y0);
|
||||
|
||||
int c00 = opaqueArgb(image.getPixel(x0, y0));
|
||||
int c10 = opaqueArgb(image.getPixel(x1, y0));
|
||||
int c01 = opaqueArgb(image.getPixel(x0, y1));
|
||||
int c11 = opaqueArgb(image.getPixel(x1, y1));
|
||||
|
||||
int red = bilerp(ARGB.red(c00), ARGB.red(c10), ARGB.red(c01), ARGB.red(c11), tx, ty);
|
||||
int green = bilerp(ARGB.green(c00), ARGB.green(c10), ARGB.green(c01), ARGB.green(c11), tx, ty);
|
||||
int blue = bilerp(ARGB.blue(c00), ARGB.blue(c10), ARGB.blue(c01), ARGB.blue(c11), tx, ty);
|
||||
return ARGB.color(255, red, green, blue);
|
||||
}
|
||||
|
||||
private static int opaqueArgb(int argb) {
|
||||
return ARGB.opaque(argb);
|
||||
}
|
||||
|
||||
private static int bilerp(int c00, int c10, int c01, int c11, float tx, float ty) {
|
||||
float top = lerp(c00, c10, tx);
|
||||
float bottom = lerp(c01, c11, tx);
|
||||
return Math.round(lerp(top, bottom, ty));
|
||||
}
|
||||
|
||||
private static float lerp(float from, float to, float delta) {
|
||||
return from + (to - from) * delta;
|
||||
}
|
||||
|
||||
private static float clamp01(float value) {
|
||||
return Math.max(0.0F, Math.min(1.0F, value));
|
||||
}
|
||||
|
||||
private record PendingCapture(ItemStack cameraStack, PhotoStyle style) {
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package fr.koka99cab.canon.client.hud;
|
||||
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.canon.item.CameraItem;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class CameraViewfinderHud {
|
||||
private static final int OUTER_SHADE = 0xA6000000;
|
||||
private static final int INNER_SHADE = 0x4C000000;
|
||||
private static final int FRAME_COLOR = 0xD8E5DED2;
|
||||
private static final int CORNER_COLOR = 0xFFF5EFE2;
|
||||
private static final int FRAME_THICKNESS = 2;
|
||||
|
||||
private CameraViewfinderHud() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
HudElementRegistry.attachElementAfter(VanillaHudElements.MISC_OVERLAYS, CanonMod.id("camera_viewfinder"), CameraViewfinderHud::render);
|
||||
}
|
||||
|
||||
private static void render(GuiGraphicsExtractor context, DeltaTracker tickCounter) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (!shouldRender(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int width = context.guiWidth();
|
||||
int height = context.guiHeight();
|
||||
CameraViewfinderLayout.Square square = CameraViewfinderLayout.forWindow(width, height);
|
||||
int left = square.left();
|
||||
int top = square.top();
|
||||
int right = square.right();
|
||||
int bottom = square.bottom();
|
||||
|
||||
// Darken everything outside the central square so the viewfinder reads clearly.
|
||||
context.fill(0, 0, width, top, OUTER_SHADE);
|
||||
context.fill(0, bottom, width, height, OUTER_SHADE);
|
||||
context.fill(0, top, left, bottom, OUTER_SHADE);
|
||||
context.fill(right, top, width, bottom, OUTER_SHADE);
|
||||
|
||||
// Add a soft inner rim to make the clear area feel recessed into the camera body.
|
||||
context.fill(left, top, right, top + FRAME_THICKNESS, INNER_SHADE);
|
||||
context.fill(left, bottom - FRAME_THICKNESS, right, bottom, INNER_SHADE);
|
||||
context.fill(left, top, left + FRAME_THICKNESS, bottom, INNER_SHADE);
|
||||
context.fill(right - FRAME_THICKNESS, top, right, bottom, INNER_SHADE);
|
||||
|
||||
drawFrame(context, left, top, right, bottom);
|
||||
}
|
||||
|
||||
private static boolean shouldRender(Minecraft client) {
|
||||
if (client.player == null || client.level == null || client.screen != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!client.options.getCameraType().isFirstPerson()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isCamera(client.player.getMainHandItem()) || isCamera(client.player.getOffhandItem());
|
||||
}
|
||||
|
||||
private static boolean isCamera(ItemStack stack) {
|
||||
return stack.getItem() instanceof CameraItem;
|
||||
}
|
||||
|
||||
private static void drawFrame(GuiGraphicsExtractor context, int left, int top, int right, int bottom) {
|
||||
int cornerLength = Math.max(12, (right - left) / 10);
|
||||
|
||||
context.fill(left, top, right, top + 1, FRAME_COLOR);
|
||||
context.fill(left, bottom - 1, right, bottom, FRAME_COLOR);
|
||||
context.fill(left, top, left + 1, bottom, FRAME_COLOR);
|
||||
context.fill(right - 1, top, right, bottom, FRAME_COLOR);
|
||||
|
||||
context.fill(left, top, left + cornerLength, top + FRAME_THICKNESS, CORNER_COLOR);
|
||||
context.fill(left, top, left + FRAME_THICKNESS, top + cornerLength, CORNER_COLOR);
|
||||
context.fill(right - cornerLength, top, right, top + FRAME_THICKNESS, CORNER_COLOR);
|
||||
context.fill(right - FRAME_THICKNESS, top, right, top + cornerLength, CORNER_COLOR);
|
||||
context.fill(left, bottom - FRAME_THICKNESS, left + cornerLength, bottom, CORNER_COLOR);
|
||||
context.fill(left, bottom - cornerLength, left + FRAME_THICKNESS, bottom, CORNER_COLOR);
|
||||
context.fill(right - cornerLength, bottom - FRAME_THICKNESS, right, bottom, CORNER_COLOR);
|
||||
context.fill(right - FRAME_THICKNESS, bottom - cornerLength, right, bottom, CORNER_COLOR);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package fr.koka99cab.canon.client.hud;
|
||||
|
||||
public final class CameraViewfinderLayout {
|
||||
private static final float VIEWFINDER_SCALE = 0.72F;
|
||||
private static final int MIN_VIEWFINDER_SIZE = 96;
|
||||
|
||||
private CameraViewfinderLayout() {
|
||||
}
|
||||
|
||||
public static Square forWindow(int width, int height) {
|
||||
int squareSize = Math.max(MIN_VIEWFINDER_SIZE, Math.round(Math.min(width, height) * VIEWFINDER_SCALE));
|
||||
int left = (width - squareSize) / 2;
|
||||
int top = (height - squareSize) / 2;
|
||||
return new Square(left, top, squareSize);
|
||||
}
|
||||
|
||||
public record Square(int left, int top, int size) {
|
||||
public int right() {
|
||||
return this.left + this.size;
|
||||
}
|
||||
|
||||
public int bottom() {
|
||||
return this.top + this.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package fr.koka99cab.canon.client.item;
|
||||
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.canon.item.FilmItem;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import fr.koka99cab.canon.registry.CanonItems;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import net.minecraft.client.color.item.ItemTintSource;
|
||||
import net.minecraft.client.color.item.ItemTintSources;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public record FilmTintSource(int defaultColor) implements ItemTintSource {
|
||||
public static final MapCodec<FilmTintSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("default", ARGB.opaque(PhotoStyle.WHITE.accentColor())).forGetter(FilmTintSource::defaultColor)
|
||||
).apply(instance, FilmTintSource::new));
|
||||
|
||||
public static void register() {
|
||||
try {
|
||||
Field idMapperField = ItemTintSources.class.getDeclaredField("ID_MAPPER");
|
||||
idMapperField.setAccessible(true);
|
||||
Object idMapper = idMapperField.get(null);
|
||||
Method put = idMapper.getClass().getMethod("put", Object.class, Object.class);
|
||||
put.invoke(idMapper, CanonMod.id("film_style"), CODEC);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
CanonMod.LOGGER.warn("Failed to register film item tint source", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<FilmTintSource> type() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculate(ItemStack stack, ClientLevel world, LivingEntity user) {
|
||||
if (stack.getItem() != CanonItems.FILM) {
|
||||
return this.defaultColor;
|
||||
}
|
||||
|
||||
PhotoStyle style = FilmItem.getStyle(stack);
|
||||
return style == null ? this.defaultColor : ARGB.opaque(style.accentColor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.koka99cab.canon;
|
||||
|
||||
public final class CanonConstants {
|
||||
public static final int CAMERA_CAPACITY = 8;
|
||||
public static final int MAP_SIZE = 128;
|
||||
public static final int PHOTO_PIXEL_COUNT = MAP_SIZE * MAP_SIZE;
|
||||
public static final int CAMERA_COOLDOWN_TICKS = 10;
|
||||
|
||||
private CanonConstants() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka99cab.canon;
|
||||
|
||||
import fr.koka99cab.canon.component.CanonDataComponents;
|
||||
import fr.koka99cab.canon.network.CanonNetworking;
|
||||
import fr.koka99cab.canon.registry.CanonItems;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class CanonMod implements ModInitializer {
|
||||
public static final String MOD_ID = "canon";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public static Identifier id(String path) {
|
||||
return Identifier.fromNamespaceAndPath(MOD_ID, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
CanonDataComponents.initialize();
|
||||
CanonItems.initialize();
|
||||
CanonNetworking.initialize();
|
||||
LOGGER.info("Canon loaded");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fr.koka99cab.canon.client;
|
||||
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class CanonClientBridge {
|
||||
private static CaptureHandler captureHandler = (cameraStack, style) -> {
|
||||
};
|
||||
|
||||
private CanonClientBridge() {
|
||||
}
|
||||
|
||||
public static void setCaptureHandler(CaptureHandler captureHandler) {
|
||||
CanonClientBridge.captureHandler = captureHandler;
|
||||
}
|
||||
|
||||
public static void capture(ItemStack cameraStack, PhotoStyle style) {
|
||||
captureHandler.capture(cameraStack, style);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CaptureHandler {
|
||||
void capture(ItemStack cameraStack, PhotoStyle style);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.canon.component;
|
||||
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
|
||||
public final class CanonDataComponents {
|
||||
public static final DataComponentType<PhotoStyle> PHOTO_STYLE = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
CanonMod.id("photo_style"),
|
||||
DataComponentType.<PhotoStyle>builder()
|
||||
.persistent(PhotoStyle.CODEC)
|
||||
.networkSynchronized(PhotoStyle.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
public static final DataComponentType<LoadedFilmComponent> LOADED_FILM = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
CanonMod.id("loaded_film"),
|
||||
DataComponentType.<LoadedFilmComponent>builder()
|
||||
.persistent(LoadedFilmComponent.CODEC)
|
||||
.networkSynchronized(LoadedFilmComponent.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
|
||||
private CanonDataComponents() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package fr.koka99cab.canon.component;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
public record LoadedFilmComponent(Optional<PhotoStyle> style, int count) {
|
||||
public static final LoadedFilmComponent EMPTY = new LoadedFilmComponent(Optional.empty(), 0);
|
||||
public static final Codec<LoadedFilmComponent> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
PhotoStyle.CODEC.optionalFieldOf("style").forGetter(component -> component.style),
|
||||
Codec.intRange(0, CanonConstants.CAMERA_CAPACITY).fieldOf("count").forGetter(component -> component.count)
|
||||
).apply(instance, LoadedFilmComponent::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, LoadedFilmComponent> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.optional(PhotoStyle.STREAM_CODEC),
|
||||
LoadedFilmComponent::style,
|
||||
ByteBufCodecs.VAR_INT,
|
||||
LoadedFilmComponent::count,
|
||||
LoadedFilmComponent::new
|
||||
);
|
||||
|
||||
public LoadedFilmComponent {
|
||||
style = style == null ? Optional.empty() : style;
|
||||
if (count < 0 || count > CanonConstants.CAMERA_CAPACITY) {
|
||||
throw new IllegalArgumentException("Film count out of bounds: " + count);
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
style = Optional.empty();
|
||||
} else if (style.isEmpty()) {
|
||||
throw new IllegalArgumentException("Loaded camera requires a style");
|
||||
}
|
||||
}
|
||||
|
||||
public static LoadedFilmComponent of(PhotoStyle style, int count) {
|
||||
return count <= 0 ? EMPTY : new LoadedFilmComponent(Optional.of(style), Math.min(count, CanonConstants.CAMERA_CAPACITY));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return this.count == 0;
|
||||
}
|
||||
|
||||
public boolean canAccept(PhotoStyle incomingStyle) {
|
||||
return this.count < CanonConstants.CAMERA_CAPACITY && (this.style.isEmpty() || this.style.filter(incomingStyle::equals).isPresent());
|
||||
}
|
||||
|
||||
public int remainingSpace() {
|
||||
return CanonConstants.CAMERA_CAPACITY - this.count;
|
||||
}
|
||||
|
||||
public LoadedFilmComponent add(PhotoStyle incomingStyle, int amount) {
|
||||
if (amount <= 0 || !this.canAccept(incomingStyle)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return of(incomingStyle, Math.min(CanonConstants.CAMERA_CAPACITY, this.count + amount));
|
||||
}
|
||||
|
||||
public LoadedFilmComponent decrement() {
|
||||
if (this.count <= 1) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return new LoadedFilmComponent(this.style, this.count - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package fr.koka99cab.canon.item;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.client.CanonClientBridge;
|
||||
import fr.koka99cab.canon.component.CanonDataComponents;
|
||||
import fr.koka99cab.canon.component.LoadedFilmComponent;
|
||||
import fr.koka99cab.canon.registry.CanonItems;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.SlotAccess;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.ClickAction;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public class CameraItem extends Item {
|
||||
public CameraItem(Properties settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean overrideOtherStackedOnMe(ItemStack stack, ItemStack otherStack, Slot slot, ClickAction clickAction, Player player, SlotAccess carriedSlot) {
|
||||
if (!otherStack.isEmpty() && otherStack.getItem() == CanonItems.FILM) {
|
||||
return tryInsertFilm(stack, otherStack, player);
|
||||
}
|
||||
|
||||
if (otherStack.isEmpty() && getLoadedFilm(stack).count() > 0) {
|
||||
ItemStack unloaded = unloadFilm(stack);
|
||||
if (!unloaded.isEmpty()) {
|
||||
playSound(player, SoundEvents.BUNDLE_REMOVE_ONE, 0.8F, 1.2F);
|
||||
carriedSlot.set(unloaded);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean overrideStackedOnOther(ItemStack stack, Slot slot, ClickAction clickAction, Player player) {
|
||||
ItemStack slotStack = slot.getItem();
|
||||
if (!slotStack.isEmpty() && slotStack.getItem() == CanonItems.FILM) {
|
||||
boolean changed = tryInsertFilm(stack, slotStack, player);
|
||||
if (changed) {
|
||||
slot.setChanged();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (slotStack.isEmpty() && getLoadedFilm(stack).count() > 0) {
|
||||
ItemStack unloaded = unloadFilm(stack);
|
||||
if (!unloaded.isEmpty()) {
|
||||
ItemStack leftover = slot.safeInsert(unloaded);
|
||||
if (leftover.isEmpty()) {
|
||||
playSound(player, SoundEvents.BUNDLE_REMOVE_ONE, 0.8F, 1.2F);
|
||||
} else {
|
||||
stack.set(CanonDataComponents.LOADED_FILM, LoadedFilmComponent.of(FilmItem.getStyle(leftover), leftover.getCount()));
|
||||
}
|
||||
slot.setChanged();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level world, Player user, InteractionHand hand) {
|
||||
ItemStack cameraStack = user.getItemInHand(hand);
|
||||
LoadedFilmComponent loadedFilm = getLoadedFilm(cameraStack);
|
||||
if (loadedFilm.isEmpty() || loadedFilm.style().isEmpty()) {
|
||||
if (world.isClientSide()) {
|
||||
user.sendOverlayMessage(Component.translatable("message.canon.camera.not_loaded"));
|
||||
}
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
|
||||
if (user.getCooldowns().isOnCooldown(cameraStack)) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
|
||||
if (world.isClientSide()) {
|
||||
CanonClientBridge.capture(cameraStack, loadedFilm.style().orElseThrow());
|
||||
}
|
||||
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBarVisible(ItemStack stack) {
|
||||
return getLoadedFilm(stack).count() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBarWidth(ItemStack stack) {
|
||||
return Math.max(1, Math.min(13, Math.round((13.0F * getLoadedFilm(stack).count()) / CanonConstants.CAMERA_CAPACITY)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBarColor(ItemStack stack) {
|
||||
return getLoadedFilm(stack).style().map(PhotoStyle::accentColor).orElse(0x8A8A8A);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack,
|
||||
TooltipContext context,
|
||||
TooltipDisplay displayComponent,
|
||||
Consumer<Component> textConsumer,
|
||||
TooltipFlag type
|
||||
) {
|
||||
LoadedFilmComponent loadedFilm = getLoadedFilm(stack);
|
||||
if (loadedFilm.isEmpty() || loadedFilm.style().isEmpty()) {
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.camera.empty").withStyle(ChatFormatting.GRAY));
|
||||
} else {
|
||||
PhotoStyle style = loadedFilm.style().orElseThrow();
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.camera.loaded", style.displayName()).withStyle(ChatFormatting.GRAY));
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.camera.shots", loadedFilm.count(), CanonConstants.CAMERA_CAPACITY).withStyle(ChatFormatting.BLUE));
|
||||
}
|
||||
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.camera.controls").withStyle(ChatFormatting.DARK_GRAY));
|
||||
}
|
||||
|
||||
public static LoadedFilmComponent getLoadedFilm(ItemStack stack) {
|
||||
return stack.getOrDefault(CanonDataComponents.LOADED_FILM, LoadedFilmComponent.EMPTY);
|
||||
}
|
||||
|
||||
private static boolean tryInsertFilm(ItemStack cameraStack, ItemStack filmStack, Player player) {
|
||||
PhotoStyle style = FilmItem.getStyle(filmStack);
|
||||
if (style == null) {
|
||||
sendActionBar(player, "message.canon.camera.invalid_film");
|
||||
playSound(player, SoundEvents.BUNDLE_INSERT_FAIL, 0.8F, 1.0F);
|
||||
return false;
|
||||
}
|
||||
|
||||
LoadedFilmComponent loadedFilm = getLoadedFilm(cameraStack);
|
||||
if (!loadedFilm.canAccept(style)) {
|
||||
sendActionBar(player, "message.canon.camera.mismatched_film");
|
||||
playSound(player, SoundEvents.BUNDLE_INSERT_FAIL, 0.8F, 1.0F);
|
||||
return false;
|
||||
}
|
||||
|
||||
int moved = Math.min(loadedFilm.remainingSpace(), filmStack.getCount());
|
||||
if (moved <= 0) {
|
||||
sendActionBar(player, "message.canon.camera.full");
|
||||
playSound(player, SoundEvents.BUNDLE_INSERT_FAIL, 0.8F, 1.0F);
|
||||
return false;
|
||||
}
|
||||
|
||||
filmStack.shrink(moved);
|
||||
cameraStack.set(CanonDataComponents.LOADED_FILM, loadedFilm.add(style, moved));
|
||||
playSound(player, SoundEvents.BUNDLE_INSERT, 0.8F, 1.15F);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ItemStack unloadFilm(ItemStack cameraStack) {
|
||||
LoadedFilmComponent loadedFilm = getLoadedFilm(cameraStack);
|
||||
if (loadedFilm.isEmpty() || loadedFilm.style().isEmpty()) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
ItemStack unloaded = FilmItem.createStack(CanonItems.FILM, loadedFilm.style().orElseThrow(), loadedFilm.count());
|
||||
cameraStack.set(CanonDataComponents.LOADED_FILM, LoadedFilmComponent.EMPTY);
|
||||
return unloaded;
|
||||
}
|
||||
|
||||
private static void sendActionBar(Player player, String translationKey) {
|
||||
player.sendOverlayMessage(Component.translatable(translationKey));
|
||||
}
|
||||
|
||||
private static void playSound(Player player, SoundEvent sound, float volume, float pitch) {
|
||||
player.level().playSound(null, player.blockPosition(), sound, SoundSource.PLAYERS, volume, pitch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package fr.koka99cab.canon.item;
|
||||
|
||||
import fr.koka99cab.canon.component.CanonDataComponents;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
public class FilmItem extends Item {
|
||||
public FilmItem(Properties settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
public static ItemStack createStack(Item item, PhotoStyle style, int count) {
|
||||
ItemStack stack = new ItemStack(item, count);
|
||||
stack.set(CanonDataComponents.PHOTO_STYLE, style);
|
||||
return stack;
|
||||
}
|
||||
|
||||
public static PhotoStyle getStyle(ItemStack stack) {
|
||||
return stack.get(CanonDataComponents.PHOTO_STYLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
PhotoStyle style = getStyle(stack);
|
||||
return style == null ? super.getName(stack) : Component.translatable("item.canon.film.named", style.displayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack,
|
||||
TooltipContext context,
|
||||
TooltipDisplay displayComponent,
|
||||
Consumer<Component> textConsumer,
|
||||
TooltipFlag type
|
||||
) {
|
||||
PhotoStyle style = getStyle(stack);
|
||||
if (style == null) {
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.film.invalid").withStyle(ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.film.style", style.displayName()).withStyle(ChatFormatting.GRAY));
|
||||
textConsumer.accept(style.effectDescription().copy().withStyle(ChatFormatting.DARK_GRAY));
|
||||
textConsumer.accept(Component.empty());
|
||||
textConsumer.accept(Component.translatable("tooltip.canon.film.shot_value").withStyle(ChatFormatting.BLUE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package fr.koka99cab.canon.item;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.item.Item;
|
||||
|
||||
public enum PhotoStyle {
|
||||
WHITE("white", "minecraft:white_dye", 0xF4F1E4, 0.02F, 1.00F, 1.00F, 1.00F, false, false),
|
||||
LIGHT_GRAY("light_gray", "minecraft:light_gray_dye", 0xDBD7CF, 0.18F, 0.75F, 0.95F, 1.06F, false, false),
|
||||
GRAY("gray", "minecraft:gray_dye", 0x86827A, 0.08F, 0.12F, 1.30F, 0.92F, true, false),
|
||||
BLACK("black", "minecraft:black_dye", 0x292929, 0.00F, 0.00F, 1.18F, 0.82F, true, false),
|
||||
BROWN("brown", "minecraft:brown_dye", 0x8F5F3A, 0.30F, 0.55F, 1.02F, 0.98F, false, true),
|
||||
RED("red", "minecraft:red_dye", 0xBE5136, 0.22F, 1.10F, 1.06F, 0.98F, false, false),
|
||||
ORANGE("orange", "minecraft:orange_dye", 0xDA8A3A, 0.26F, 1.08F, 0.98F, 1.02F, false, false),
|
||||
YELLOW("yellow", "minecraft:yellow_dye", 0xE2C15D, 0.18F, 0.72F, 0.90F, 1.08F, false, false),
|
||||
LIME("lime", "minecraft:lime_dye", 0x8FD744, 0.25F, 1.28F, 1.12F, 1.00F, false, false),
|
||||
GREEN("green", "minecraft:green_dye", 0x4E8A4C, 0.28F, 0.92F, 1.04F, 0.95F, false, false),
|
||||
CYAN("cyan", "minecraft:cyan_dye", 0x4C8B92, 0.22F, 0.92F, 0.94F, 1.02F, false, false),
|
||||
LIGHT_BLUE("light_blue", "minecraft:light_blue_dye", 0x8DB6D9, 0.18F, 0.84F, 0.90F, 1.10F, false, false),
|
||||
BLUE("blue", "minecraft:blue_dye", 0x4E66B4, 0.26F, 0.96F, 1.10F, 0.92F, false, false),
|
||||
PURPLE("purple", "minecraft:purple_dye", 0x8460AF, 0.24F, 1.00F, 1.06F, 0.94F, false, false),
|
||||
MAGENTA("magenta", "minecraft:magenta_dye", 0xC865B2, 0.28F, 1.12F, 1.08F, 1.02F, false, false),
|
||||
PINK("pink", "minecraft:pink_dye", 0xE4A8C6, 0.22F, 0.86F, 0.90F, 1.12F, false, false);
|
||||
|
||||
private static final Map<String, PhotoStyle> BY_ID = Arrays.stream(values()).collect(Collectors.toUnmodifiableMap(PhotoStyle::id, Function.identity()));
|
||||
private static final Map<String, PhotoStyle> BY_DYE_ID = Arrays.stream(values()).collect(Collectors.toUnmodifiableMap(PhotoStyle::dyeId, Function.identity()));
|
||||
public static final Codec<PhotoStyle> CODEC = Codec.STRING.comapFlatMap(
|
||||
id -> PhotoStyle.byId(id).map(DataResult::success).orElseGet(() -> DataResult.error(() -> "Unknown photo style: " + id)),
|
||||
PhotoStyle::id
|
||||
);
|
||||
public static final StreamCodec<ByteBuf, PhotoStyle> STREAM_CODEC = ByteBufCodecs.STRING_UTF8.map(PhotoStyle::require, PhotoStyle::id);
|
||||
|
||||
private final String id;
|
||||
private final String dyeId;
|
||||
private final int accentColor;
|
||||
private final float tintStrength;
|
||||
private final float saturation;
|
||||
private final float contrast;
|
||||
private final float brightness;
|
||||
private final boolean monochrome;
|
||||
private final boolean sepia;
|
||||
|
||||
PhotoStyle(
|
||||
String id,
|
||||
String dyeId,
|
||||
int accentColor,
|
||||
float tintStrength,
|
||||
float saturation,
|
||||
float contrast,
|
||||
float brightness,
|
||||
boolean monochrome,
|
||||
boolean sepia
|
||||
) {
|
||||
this.id = id;
|
||||
this.dyeId = dyeId;
|
||||
this.accentColor = accentColor;
|
||||
this.tintStrength = tintStrength;
|
||||
this.saturation = saturation;
|
||||
this.contrast = contrast;
|
||||
this.brightness = brightness;
|
||||
this.monochrome = monochrome;
|
||||
this.sepia = sepia;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String dyeId() {
|
||||
return this.dyeId;
|
||||
}
|
||||
|
||||
public int accentColor() {
|
||||
return this.accentColor;
|
||||
}
|
||||
|
||||
public Component displayName() {
|
||||
return Component.translatable("style.canon." + this.id);
|
||||
}
|
||||
|
||||
public Component effectDescription() {
|
||||
return Component.translatable("style.canon." + this.id + ".effect");
|
||||
}
|
||||
|
||||
public int applyFilter(int argb) {
|
||||
float r = ARGB.red(argb) / 255.0F;
|
||||
float g = ARGB.green(argb) / 255.0F;
|
||||
float b = ARGB.blue(argb) / 255.0F;
|
||||
|
||||
float luminance = r * 0.299F + g * 0.587F + b * 0.114F;
|
||||
if (this.monochrome) {
|
||||
r = luminance;
|
||||
g = luminance;
|
||||
b = luminance;
|
||||
} else if (this.sepia) {
|
||||
float newR = clamp(luminance * 1.15F + 0.18F);
|
||||
float newG = clamp(luminance * 1.00F + 0.05F);
|
||||
float newB = clamp(luminance * 0.82F);
|
||||
r = newR;
|
||||
g = newG;
|
||||
b = newB;
|
||||
}
|
||||
|
||||
luminance = r * 0.299F + g * 0.587F + b * 0.114F;
|
||||
r = luminance + (r - luminance) * this.saturation;
|
||||
g = luminance + (g - luminance) * this.saturation;
|
||||
b = luminance + (b - luminance) * this.saturation;
|
||||
|
||||
float tintR = ARGB.red(this.accentColor) / 255.0F;
|
||||
float tintG = ARGB.green(this.accentColor) / 255.0F;
|
||||
float tintB = ARGB.blue(this.accentColor) / 255.0F;
|
||||
r = lerp(r, tintR, this.tintStrength);
|
||||
g = lerp(g, tintG, this.tintStrength);
|
||||
b = lerp(b, tintB, this.tintStrength);
|
||||
|
||||
r = clamp((r - 0.5F) * this.contrast + 0.5F);
|
||||
g = clamp((g - 0.5F) * this.contrast + 0.5F);
|
||||
b = clamp((b - 0.5F) * this.contrast + 0.5F);
|
||||
|
||||
r = clamp(r * this.brightness);
|
||||
g = clamp(g * this.brightness);
|
||||
b = clamp(b * this.brightness);
|
||||
|
||||
return ARGB.color(255, Math.round(r * 255.0F), Math.round(g * 255.0F), Math.round(b * 255.0F));
|
||||
}
|
||||
|
||||
public static Optional<PhotoStyle> byId(String id) {
|
||||
return Optional.ofNullable(BY_ID.get(id.toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
|
||||
public static PhotoStyle require(String id) {
|
||||
return byId(id).orElseThrow(() -> new IllegalArgumentException("Unknown photo style: " + id));
|
||||
}
|
||||
|
||||
public static Optional<PhotoStyle> fromDye(Item item) {
|
||||
return Optional.ofNullable(BY_DYE_ID.get(BuiltInRegistries.ITEM.getKey(item).toString()));
|
||||
}
|
||||
|
||||
public static Optional<PhotoStyle> fromDyeId(String dyeId) {
|
||||
return Optional.ofNullable(BY_DYE_ID.get(dyeId.toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
|
||||
private static float lerp(float from, float to, float delta) {
|
||||
return from + (to - from) * delta;
|
||||
}
|
||||
|
||||
private static float clamp(float value) {
|
||||
return Math.max(0.0F, Math.min(1.0F, value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package fr.koka99cab.canon.network;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.component.CanonDataComponents;
|
||||
import fr.koka99cab.canon.component.LoadedFilmComponent;
|
||||
import fr.koka99cab.canon.item.CameraItem;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import fr.koka99cab.canon.photo.PhotoMapService;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class CanonNetworking {
|
||||
private CanonNetworking() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
PayloadTypeRegistry.serverboundPlay().register(CapturePhotoPayload.ID, CapturePhotoPayload.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(CapturePhotoPayload.ID, (payload, context) -> handleCapture(context.player(), payload));
|
||||
}
|
||||
|
||||
private static void handleCapture(ServerPlayer player, CapturePhotoPayload payload) {
|
||||
if (payload.colors().length != CanonConstants.PHOTO_PIXEL_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InteractionHand> cameraHand = findLoadedCameraHand(player);
|
||||
if (cameraHand.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack cameraStack = player.getItemInHand(cameraHand.orElseThrow());
|
||||
if (player.getCooldowns().isOnCooldown(cameraStack)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadedFilmComponent loadedFilm = cameraStack.getOrDefault(CanonDataComponents.LOADED_FILM, LoadedFilmComponent.EMPTY);
|
||||
if (loadedFilm.isEmpty() || loadedFilm.style().isEmpty() || loadedFilm.count() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PhotoStyle style = loadedFilm.style().orElseThrow();
|
||||
cameraStack.set(CanonDataComponents.LOADED_FILM, loadedFilm.decrement());
|
||||
player.getCooldowns().addCooldown(cameraStack, CanonConstants.CAMERA_COOLDOWN_TICKS);
|
||||
|
||||
ItemStack photo = PhotoMapService.createPhoto(player, style, Arrays.copyOf(payload.colors(), payload.colors().length));
|
||||
boolean inserted = player.getInventory().add(photo);
|
||||
if (!inserted) {
|
||||
player.drop(photo, false);
|
||||
}
|
||||
|
||||
player.awardStat(Stats.ITEM_USED.get(cameraStack.getItem()));
|
||||
player.sendOverlayMessage(Component.translatable("message.canon.camera.captured"));
|
||||
}
|
||||
|
||||
private static Optional<InteractionHand> findLoadedCameraHand(Player player) {
|
||||
for (InteractionHand hand : InteractionHand.values()) {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (stack.getItem() instanceof CameraItem && !CameraItem.getLoadedFilm(stack).isEmpty()) {
|
||||
return Optional.of(hand);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package fr.koka99cab.canon.network;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
public record CapturePhotoPayload(byte[] colors) implements CustomPacketPayload {
|
||||
public static final Type<CapturePhotoPayload> ID = new Type<>(CanonMod.id("capture_photo"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, CapturePhotoPayload> CODEC = CustomPacketPayload.codec(CapturePhotoPayload::write, CapturePhotoPayload::new);
|
||||
|
||||
public CapturePhotoPayload {
|
||||
colors = colors.clone();
|
||||
if (colors.length != CanonConstants.PHOTO_PIXEL_COUNT) {
|
||||
throw new IllegalArgumentException("Expected " + CanonConstants.PHOTO_PIXEL_COUNT + " colors, got " + colors.length);
|
||||
}
|
||||
}
|
||||
|
||||
public CapturePhotoPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this(buffer.readByteArray(CanonConstants.PHOTO_PIXEL_COUNT));
|
||||
}
|
||||
|
||||
public byte[] colors() {
|
||||
return this.colors.clone();
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeByteArray(this.colors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package fr.koka99cab.canon.photo;
|
||||
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
|
||||
public final class PhotoMapPalette {
|
||||
private static final int[] COLORS = new int[256];
|
||||
|
||||
static {
|
||||
for (int i = 0; i < COLORS.length; i++) {
|
||||
COLORS[i] = MapColor.getColorFromPackedId(i);
|
||||
}
|
||||
}
|
||||
|
||||
private PhotoMapPalette() {
|
||||
}
|
||||
|
||||
public static byte nearestColor(int argb) {
|
||||
int red = ARGB.red(argb);
|
||||
int green = ARGB.green(argb);
|
||||
int blue = ARGB.blue(argb);
|
||||
int bestDistance = Integer.MAX_VALUE;
|
||||
int bestIndex = 0;
|
||||
|
||||
for (int i = 0; i < COLORS.length; i++) {
|
||||
int palette = COLORS[i];
|
||||
if (ARGB.alpha(palette) == 0) {
|
||||
continue;
|
||||
}
|
||||
int paletteRed = ARGB.red(palette);
|
||||
int paletteGreen = ARGB.green(palette);
|
||||
int paletteBlue = ARGB.blue(palette);
|
||||
int distance = square(red - paletteRed) * 30 + square(green - paletteGreen) * 59 + square(blue - paletteBlue) * 11;
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return (byte)bestIndex;
|
||||
}
|
||||
|
||||
static int colorAt(int index) {
|
||||
return COLORS[index & 0xFF];
|
||||
}
|
||||
|
||||
private static int square(int value) {
|
||||
return value * value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package fr.koka99cab.canon.photo;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import fr.koka99cab.canon.registry.CanonItems;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
|
||||
|
||||
public final class PhotoMapService {
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ROOT);
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss z", Locale.ROOT);
|
||||
|
||||
private PhotoMapService() {
|
||||
}
|
||||
|
||||
public static ItemStack createPhoto(ServerPlayer player, PhotoStyle style, byte[] colors) {
|
||||
ServerLevel world = player.level();
|
||||
BlockPos capturePos = player.blockPosition();
|
||||
Identifier biomeId = world.getBiome(capturePos)
|
||||
.unwrapKey()
|
||||
.map(ResourceKey<Biome>::identifier)
|
||||
.orElse(CanonMod.id("unknown_biome"));
|
||||
ZonedDateTime capturedAt = ZonedDateTime.now();
|
||||
String capturedDate = DATE_FORMATTER.format(capturedAt);
|
||||
String capturedTime = TIME_FORMATTER.format(capturedAt);
|
||||
String authorName = player.getName().getString();
|
||||
Component biomeName = Component.translatableWithFallback(biomeId.toLanguageKey("biome"), formatBiomeName(biomeId));
|
||||
|
||||
MapItemSavedData unlocked = MapItemSavedData.createFresh(player.getX(), player.getZ(), (byte)0, false, false, world.dimension());
|
||||
for (int y = 0; y < CanonConstants.MAP_SIZE; y++) {
|
||||
for (int x = 0; x < CanonConstants.MAP_SIZE; x++) {
|
||||
unlocked.setColor(x, y, colors[x + y * CanonConstants.MAP_SIZE]);
|
||||
}
|
||||
}
|
||||
|
||||
MapItemSavedData locked = unlocked.locked();
|
||||
MapId id = world.getFreeMapId();
|
||||
world.setMapData(id, locked);
|
||||
|
||||
ItemStack photo = new ItemStack(Items.FILLED_MAP);
|
||||
photo.set(DataComponents.MAP_ID, id);
|
||||
photo.set(DataComponents.CUSTOM_NAME, photoName(style, biomeName));
|
||||
photo.set(DataComponents.LORE, new ItemLore(List.of(
|
||||
loreLine("tooltip.canon.photo.author", ChatFormatting.GOLD, Component.literal(authorName)),
|
||||
loreLine("tooltip.canon.photo.biome", ChatFormatting.DARK_AQUA, biomeName.copy()),
|
||||
loreLine("tooltip.canon.photo.date", ChatFormatting.GRAY, Component.literal(capturedDate)),
|
||||
loreLine("tooltip.canon.photo.time", ChatFormatting.GRAY, Component.literal(capturedTime)),
|
||||
loreLine("tooltip.canon.photo.position", ChatFormatting.GRAY, capturePos.getX(), capturePos.getY(), capturePos.getZ())
|
||||
)));
|
||||
CustomData metadata = CanonItems.createPhotoMetadata(
|
||||
style,
|
||||
authorName,
|
||||
world.getServer().overworld().getGameTime(),
|
||||
biomeId,
|
||||
capturedDate,
|
||||
capturedTime,
|
||||
capturePos
|
||||
);
|
||||
photo.set(DataComponents.CUSTOM_DATA, metadata);
|
||||
return photo;
|
||||
}
|
||||
|
||||
private static Component photoName(PhotoStyle style, Component biomeName) {
|
||||
return Component.translatable("item.canon.photo.named", style.displayName().copy(), biomeName.copy())
|
||||
.withStyle(ChatFormatting.AQUA)
|
||||
.withStyle(textStyle -> textStyle.withItalic(false));
|
||||
}
|
||||
|
||||
private static Component loreLine(String key, ChatFormatting color, Object... args) {
|
||||
return Component.translatable(key, args)
|
||||
.withStyle(color)
|
||||
.withStyle(style -> style.withItalic(false));
|
||||
}
|
||||
|
||||
private static String formatBiomeName(Identifier biomeId) {
|
||||
String normalized = biomeId.getPath().replace('/', ' ').replace('_', ' ').trim();
|
||||
StringBuilder builder = new StringBuilder(normalized.length());
|
||||
boolean capitalizeNext = true;
|
||||
for (int i = 0; i < normalized.length(); i++) {
|
||||
char current = normalized.charAt(i);
|
||||
if (Character.isWhitespace(current)) {
|
||||
builder.append(current);
|
||||
capitalizeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.append(capitalizeNext ? Character.toUpperCase(current) : current);
|
||||
capitalizeNext = false;
|
||||
}
|
||||
return builder.isEmpty() ? "Unknown Biome" : builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package fr.koka99cab.canon.registry;
|
||||
|
||||
import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.canon.component.CanonDataComponents;
|
||||
import fr.koka99cab.canon.component.LoadedFilmComponent;
|
||||
import fr.koka99cab.canon.item.CameraItem;
|
||||
import fr.koka99cab.canon.item.FilmItem;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import java.util.function.Function;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
|
||||
public final class CanonItems {
|
||||
public static final Item CAMERA = registerCamera("camera");
|
||||
public static final Item CAMERA_IRON = registerCamera("camera_iron");
|
||||
public static final Item CAMERA_COPPER = registerCamera("camera_copper");
|
||||
public static final Item FILM = register("film", FilmItem::new);
|
||||
|
||||
private CanonItems() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
CreativeModeTabEvents.modifyOutputEvent(vanillaTab("tools_and_utilities")).register(entries -> {
|
||||
entries.accept(new ItemStack(CAMERA), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
|
||||
entries.accept(new ItemStack(CAMERA_IRON), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
|
||||
entries.accept(new ItemStack(CAMERA_COPPER), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
|
||||
});
|
||||
CreativeModeTabEvents.modifyOutputEvent(vanillaTab("ingredients")).register(entries -> {
|
||||
for (PhotoStyle style : PhotoStyle.values()) {
|
||||
entries.accept(FilmItem.createStack(FILM, style, 1), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Item registerCamera(String path) {
|
||||
return register(path, settings -> new CameraItem(settings
|
||||
.stacksTo(1)
|
||||
.component(CanonDataComponents.LOADED_FILM, LoadedFilmComponent.EMPTY)));
|
||||
}
|
||||
|
||||
private static Item register(String path, Function<Item.Properties, Item> factory) {
|
||||
Identifier id = CanonMod.id(path);
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id);
|
||||
Item.Properties settings = new Item.Properties().setId(key);
|
||||
return Registry.register(BuiltInRegistries.ITEM, key, factory.apply(settings));
|
||||
}
|
||||
|
||||
private static ResourceKey<CreativeModeTab> vanillaTab(String path) {
|
||||
return ResourceKey.create(Registries.CREATIVE_MODE_TAB, Identifier.fromNamespaceAndPath("minecraft", path));
|
||||
}
|
||||
|
||||
public static CustomData createPhotoMetadata(
|
||||
PhotoStyle style,
|
||||
String authorName,
|
||||
long createdAt,
|
||||
Identifier biomeId,
|
||||
String capturedDate,
|
||||
String capturedTime,
|
||||
BlockPos capturePos
|
||||
) {
|
||||
CompoundTag data = new CompoundTag();
|
||||
data.putString("style", style.id());
|
||||
data.putString("author", authorName);
|
||||
data.putLong("created_at", createdAt);
|
||||
data.putString("biome", biomeId.toString());
|
||||
data.putString("captured_date", capturedDate);
|
||||
data.putString("captured_time", capturedTime);
|
||||
data.putInt("x", capturePos.getX());
|
||||
data.putInt("y", capturePos.getY());
|
||||
data.putInt("z", capturePos.getZ());
|
||||
return CustomData.of(data);
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 573 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "canon:item/camera"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "canon:item/camera_copper"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "canon:item/camera_iron"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "canon:item/film",
|
||||
"tints": [
|
||||
{
|
||||
"type": "canon:film_style"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"item.canon.camera": "Gold Canon Camera",
|
||||
"item.canon.camera_iron": "Iron Canon Camera",
|
||||
"item.canon.camera_copper": "Copper Canon Camera",
|
||||
"item.canon.film": "Film",
|
||||
"item.canon.film.named": "%s Film",
|
||||
"item.canon.photo": "Canon Photo",
|
||||
"item.canon.photo.named": "%1$s %2$s",
|
||||
"tooltip.canon.camera.empty": "No film loaded",
|
||||
"tooltip.canon.camera.loaded": "Loaded film: %s",
|
||||
"tooltip.canon.camera.shots": "Shots remaining: %s/%s",
|
||||
"tooltip.canon.camera.controls": "Drag film onto the camera to load it. Click with an empty cursor to unload it.",
|
||||
"tooltip.canon.film.style": "Style: %s",
|
||||
"tooltip.canon.film.invalid": "This film has no style data.",
|
||||
"tooltip.canon.film.shot_value": "One film equals one shot.",
|
||||
"tooltip.canon.photo.author": "Author: %s",
|
||||
"tooltip.canon.photo.biome": "Biome: %s",
|
||||
"tooltip.canon.photo.date": "Date: %s",
|
||||
"tooltip.canon.photo.time": "Time: %s",
|
||||
"tooltip.canon.photo.position": "Position: X %s Y %s Z %s",
|
||||
"message.canon.camera.not_loaded": "The camera is not loaded.",
|
||||
"message.canon.camera.invalid_film": "This film is invalid.",
|
||||
"message.canon.camera.mismatched_film": "You cannot mix different film styles in the camera.",
|
||||
"message.canon.camera.full": "The camera is already full.",
|
||||
"message.canon.camera.captured": "Photo captured.",
|
||||
"message.canon.camera.capture_failed": "Could not capture the current frame.",
|
||||
"style.canon.white": "Neutral",
|
||||
"style.canon.white.effect": "Neutral stock with soft paper whites.",
|
||||
"style.canon.light_gray": "Matte",
|
||||
"style.canon.light_gray.effect": "Desaturated matte stock with lifted mids.",
|
||||
"style.canon.gray": "High Contrast",
|
||||
"style.canon.gray.effect": "Punchy grayscale with crisp contrast.",
|
||||
"style.canon.black": "Monochrome",
|
||||
"style.canon.black.effect": "Classic black and white stock.",
|
||||
"style.canon.brown": "Sepia",
|
||||
"style.canon.brown.effect": "Warm sepia tones with antique contrast.",
|
||||
"style.canon.red": "Warm",
|
||||
"style.canon.red.effect": "Warm reds and amber highlights.",
|
||||
"style.canon.orange": "Sunset",
|
||||
"style.canon.orange.effect": "Golden orange cast for evening scenes.",
|
||||
"style.canon.yellow": "Faded",
|
||||
"style.canon.yellow.effect": "Faded yellow wash with bright highlights.",
|
||||
"style.canon.lime": "Acid",
|
||||
"style.canon.lime.effect": "Sharp green tint with vivid saturation.",
|
||||
"style.canon.green": "Forest",
|
||||
"style.canon.green.effect": "Deep green stock for foliage-heavy shots.",
|
||||
"style.canon.cyan": "Cool",
|
||||
"style.canon.cyan.effect": "Cool cyan tint with restrained warmth.",
|
||||
"style.canon.light_blue": "Airy",
|
||||
"style.canon.light_blue.effect": "Bright airy blues with soft contrast.",
|
||||
"style.canon.blue": "Cold",
|
||||
"style.canon.blue.effect": "Cold blue stock with darker shadows.",
|
||||
"style.canon.purple": "Violet",
|
||||
"style.canon.purple.effect": "Muted violet cast for dreamy scenes.",
|
||||
"style.canon.magenta": "Pop",
|
||||
"style.canon.magenta.effect": "Bold magenta stock with lively colors.",
|
||||
"style.canon.pink": "Pastel",
|
||||
"style.canon.pink.effect": "Soft pastel pink highlights."
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"item.canon.camera": "Appareil photo Canon en or",
|
||||
"item.canon.camera_iron": "Appareil photo Canon en fer",
|
||||
"item.canon.camera_copper": "Appareil photo Canon en cuivre",
|
||||
"item.canon.film": "Pellicule",
|
||||
"item.canon.film.named": "Pellicule %s",
|
||||
"item.canon.photo": "Photo Canon",
|
||||
"item.canon.photo.named": "%1$s %2$s",
|
||||
"tooltip.canon.camera.empty": "Aucune pellicule chargee",
|
||||
"tooltip.canon.camera.loaded": "Pellicule chargee : %s",
|
||||
"tooltip.canon.camera.shots": "Cliches restants : %s/%s",
|
||||
"tooltip.canon.camera.controls": "Glissez une pellicule sur l'appareil pour la charger. Cliquez avec un curseur vide pour la retirer.",
|
||||
"tooltip.canon.film.style": "Effet : %s",
|
||||
"tooltip.canon.film.invalid": "Cette pellicule n'a pas de style valide.",
|
||||
"tooltip.canon.film.shot_value": "Une pellicule vaut une photo.",
|
||||
"tooltip.canon.photo.author": "Auteur : %s",
|
||||
"tooltip.canon.photo.biome": "Biome : %s",
|
||||
"tooltip.canon.photo.date": "Date : %s",
|
||||
"tooltip.canon.photo.time": "Heure : %s",
|
||||
"tooltip.canon.photo.position": "Position : X %s Y %s Z %s",
|
||||
"message.canon.camera.not_loaded": "L'appareil photo n'est pas charge.",
|
||||
"message.canon.camera.invalid_film": "Cette pellicule est invalide.",
|
||||
"message.canon.camera.mismatched_film": "Impossible de melanger plusieurs styles de pellicule.",
|
||||
"message.canon.camera.full": "L'appareil photo est deja plein.",
|
||||
"message.canon.camera.captured": "Photo prise.",
|
||||
"message.canon.camera.capture_failed": "Impossible de capturer l'image actuelle.",
|
||||
"style.canon.white": "Neutre",
|
||||
"style.canon.white.effect": "Rendu neutre avec des blancs doux.",
|
||||
"style.canon.light_gray": "Mat",
|
||||
"style.canon.light_gray.effect": "Pellicule desaturee au rendu mat.",
|
||||
"style.canon.gray": "Contraste",
|
||||
"style.canon.gray.effect": "Noir et blanc contraste et net.",
|
||||
"style.canon.black": "Monochrome",
|
||||
"style.canon.black.effect": "Pellicule noir et blanc classique.",
|
||||
"style.canon.brown": "Sepia",
|
||||
"style.canon.brown.effect": "Tons sepia chaleureux et vintage.",
|
||||
"style.canon.red": "Chaud",
|
||||
"style.canon.red.effect": "Dominante chaude rouge et ambree.",
|
||||
"style.canon.orange": "Coucher de soleil",
|
||||
"style.canon.orange.effect": "Teinte orange doree de fin de journee.",
|
||||
"style.canon.yellow": "Delave",
|
||||
"style.canon.yellow.effect": "Lavis jaune pale avec hautes lumieres claires.",
|
||||
"style.canon.lime": "Acide",
|
||||
"style.canon.lime.effect": "Vert vif et saturation agressive.",
|
||||
"style.canon.green": "Foret",
|
||||
"style.canon.green.effect": "Pellicule verte profonde pour les paysages.",
|
||||
"style.canon.cyan": "Froid",
|
||||
"style.canon.cyan.effect": "Dominante cyan fraiche et calme.",
|
||||
"style.canon.light_blue": "Aerien",
|
||||
"style.canon.light_blue.effect": "Bleus lumineux et contraste leger.",
|
||||
"style.canon.blue": "Glace",
|
||||
"style.canon.blue.effect": "Teinte bleue froide et ombres plus denses.",
|
||||
"style.canon.purple": "Violet",
|
||||
"style.canon.purple.effect": "Violet discret pour une ambiance reveuse.",
|
||||
"style.canon.magenta": "Pop",
|
||||
"style.canon.magenta.effect": "Magenta marque avec couleurs dynamiques.",
|
||||
"style.canon.pink": "Pastel",
|
||||
"style.canon.pink.effect": "Lumieres roses et ambiance pastel."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/handheld",
|
||||
"textures": {
|
||||
"layer0": "canon:item/camera"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/handheld",
|
||||
"textures": {
|
||||
"layer0": "canon:item/camera_copper"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/handheld",
|
||||
"textures": {
|
||||
"layer0": "canon:item/camera_iron"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "minecraft:item/paper"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 310 B |
Binary file not shown.
|
After Width: | Height: | Size: 329 B |
Binary file not shown.
|
After Width: | Height: | Size: 335 B |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": [
|
||||
"GRG",
|
||||
"GSG",
|
||||
"GGG"
|
||||
],
|
||||
"key": {
|
||||
"G": "minecraft:gold_ingot",
|
||||
"R": "minecraft:redstone",
|
||||
"S": "minecraft:spyglass"
|
||||
},
|
||||
"result": {
|
||||
"id": "canon:camera",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": [
|
||||
"CRC",
|
||||
"CSC",
|
||||
"CCC"
|
||||
],
|
||||
"key": {
|
||||
"C": "minecraft:copper_ingot",
|
||||
"R": "minecraft:redstone",
|
||||
"S": "minecraft:spyglass"
|
||||
},
|
||||
"result": {
|
||||
"id": "canon:camera_copper",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": [
|
||||
"IRI",
|
||||
"ISI",
|
||||
"III"
|
||||
],
|
||||
"key": {
|
||||
"I": "minecraft:iron_ingot",
|
||||
"R": "minecraft:redstone",
|
||||
"S": "minecraft:spyglass"
|
||||
},
|
||||
"result": {
|
||||
"id": "canon:camera_iron",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:black_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:blue_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:brown_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "brown"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:cyan_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "cyan"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:gray_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "gray"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:green_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "green"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:light_blue_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "light_blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:light_gray_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "light_gray"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:lime_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "lime"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:magenta_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "magenta"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:orange_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "orange"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:pink_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "pink"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:purple_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "purple"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:red_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "red"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:white_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:paper",
|
||||
"minecraft:yellow_dye"
|
||||
],
|
||||
"result": {
|
||||
"id": "canon:film",
|
||||
"count": 1,
|
||||
"components": {
|
||||
"canon:photo_style": "yellow"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "canon",
|
||||
"version": "${version}",
|
||||
"name": "Canon",
|
||||
"description": "Adds craftable cameras and dye-based film rolls that turn snapshots into displayable map photos.",
|
||||
"authors": [
|
||||
"KOKA99CAB"
|
||||
],
|
||||
"icon": "assets/canon/icon.png",
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"fr.koka99cab.canon.CanonMod"
|
||||
],
|
||||
"client": [
|
||||
"fr.koka99cab.canon.client.CanonClient"
|
||||
]
|
||||
},
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.2",
|
||||
"minecraft": "~26.1.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": "*"
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka99cab.canon.component;
|
||||
|
||||
import fr.koka99cab.canon.CanonConstants;
|
||||
import fr.koka99cab.canon.item.PhotoStyle;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LoadedFilmComponentTest {
|
||||
@Test
|
||||
void emptyCameraAcceptsAnyStyle() {
|
||||
LoadedFilmComponent component = LoadedFilmComponent.EMPTY.add(PhotoStyle.RED, 3);
|
||||
|
||||
assertEquals(3, component.count());
|
||||
assertEquals(PhotoStyle.RED, component.style().orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadedCameraRejectsDifferentStyle() {
|
||||
LoadedFilmComponent component = LoadedFilmComponent.of(PhotoStyle.RED, 4);
|
||||
|
||||
assertFalse(component.canAccept(PhotoStyle.BLUE));
|
||||
assertTrue(component.canAccept(PhotoStyle.RED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void decrementClearsEmptyCamera() {
|
||||
LoadedFilmComponent component = LoadedFilmComponent.of(PhotoStyle.BLACK, 1).decrement();
|
||||
|
||||
assertTrue(component.isEmpty());
|
||||
assertTrue(component.style().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addCapsAtCapacity() {
|
||||
LoadedFilmComponent component = LoadedFilmComponent.of(PhotoStyle.CYAN, CanonConstants.CAMERA_CAPACITY - 1).add(PhotoStyle.CYAN, 4);
|
||||
|
||||
assertEquals(CanonConstants.CAMERA_CAPACITY, component.count());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package fr.koka99cab.canon.item;
|
||||
|
||||
import net.minecraft.util.ARGB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PhotoStyleTest {
|
||||
@Test
|
||||
void everyStyleIsResolvableFromItsDye() {
|
||||
for (PhotoStyle style : PhotoStyle.values()) {
|
||||
assertEquals(style, PhotoStyle.byId(style.id()).orElseThrow());
|
||||
assertEquals(style, PhotoStyle.fromDyeId(style.dyeId()).orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void blackStyleProducesMonochromeOutput() {
|
||||
int filtered = PhotoStyle.BLACK.applyFilter(ARGB.color(255, 190, 110, 60));
|
||||
|
||||
assertEquals(ARGB.red(filtered), ARGB.green(filtered));
|
||||
assertEquals(ARGB.green(filtered), ARGB.blue(filtered));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stylesCoverAllVanillaDyes() {
|
||||
assertTrue(PhotoStyle.fromDyeId("minecraft:white_dye").isPresent());
|
||||
assertTrue(PhotoStyle.fromDyeId("minecraft:pink_dye").isPresent());
|
||||
assertTrue(PhotoStyle.fromDyeId("minecraft:black_dye").isPresent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package fr.koka99cab.canon.photo;
|
||||
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PhotoMapPaletteTest {
|
||||
@Test
|
||||
void exactPaletteColorReturnsMatchingIndex() {
|
||||
int opaqueIndex = 0;
|
||||
for (int i = 0; i < 256; i++) {
|
||||
if (ARGB.alpha(MapColor.getColorFromPackedId(i)) > 0) {
|
||||
opaqueIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
byte nearest = PhotoMapPalette.nearestColor(MapColor.getColorFromPackedId(opaqueIndex));
|
||||
|
||||
assertEquals(opaqueIndex, nearest & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
void quantizationIsStableForWhite() {
|
||||
byte nearest = PhotoMapPalette.nearestColor(0xFFFFFFFF);
|
||||
|
||||
assertEquals(nearest, PhotoMapPalette.nearestColor(0xFFFFFFFF));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blackDoesNotResolveToTransparentPaletteEntry() {
|
||||
byte nearest = PhotoMapPalette.nearestColor(0xFF000000);
|
||||
|
||||
assertNotEquals(0, nearest & 0xFF);
|
||||
assertTrue(ARGB.alpha(PhotoMapPalette.colorAt(nearest & 0xFF)) > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void skyBlueKeepsABluePaletteMatch() {
|
||||
byte nearest = PhotoMapPalette.nearestColor(0xFF87CEEB);
|
||||
int palette = PhotoMapPalette.colorAt(nearest & 0xFF);
|
||||
|
||||
assertTrue(ARGB.blue(palette) > ARGB.red(palette));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user