Initial sanctuary sources

This commit is contained in:
cnstiout
2026-07-04 20:27:08 +02:00
commit c796a20321
1582 changed files with 253264 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# Gradle
.gradle/
build/
out/
classes/
# Eclipse
*.launch
# IntelliJ IDEA
.idea/
*.iml
*.ipr
*.iws
# VS Code
.settings/
.vscode/
bin/
.classpath
.project
# macOS
*.DS_Store
# Fabric
run/
# Java
.jdk/
hs_err_*.log
replay_*.log
*.hprof
*.jfr
+80
View File
@@ -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 {
}
}
+21
View File
@@ -0,0 +1,21 @@
# Done to increase the memory available to Gradle.
org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
# IntelliJ IDEA is not yet fully compatible with configuration cache:
# https://github.com/FabricMC/fabric-loom/issues/1349
org.gradle.configuration-cache=false
# Fabric properties
# Check current versions on https://fabricmc.net/develop
minecraft_version=26.1.2
loader_version=0.19.2
loom_version=1.16-SNAPSHOT
# Mod properties
mod_version=1.0.0
maven_group=fr.koka99cab
archives_base_name=canon
# Dependencies
fabric_api_version=0.148.0+26.1.2
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+253
View File
@@ -0,0 +1,253 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
if [ -z "$JAVA_HOME" ] && [ -x "$APP_HOME/.jdk/current/bin/java" ] ; then
JAVA_HOME=$APP_HOME/.jdk/current
export JAVA_HOME
fi
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+93
View File
@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+12
View File
@@ -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());
}
}
@@ -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) {
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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() {
}
}
+26
View File
@@ -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);
}
}
@@ -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() {
}
}
@@ -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();
}
}
@@ -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);
}
}
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"
}
}
+11
View File
@@ -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

+18
View File
@@ -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": "*"
}
}
@@ -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));
}
}