├── changelog.md ├── src └── main │ ├── resources │ ├── icon.png │ ├── data │ │ ├── camera │ │ │ ├── tags │ │ │ │ └── item │ │ │ │ │ └── image_paper.json │ │ │ └── recipe │ │ │ │ ├── image_cloning.json │ │ │ │ ├── album.json │ │ │ │ ├── image_frame.json │ │ │ │ └── camera.json │ │ └── minecraft │ │ │ └── tags │ │ │ └── item │ │ │ └── lectern_books.json │ ├── assets │ │ └── camera │ │ │ ├── sounds │ │ │ └── take_image.ogg │ │ │ ├── textures │ │ │ ├── gui │ │ │ │ ├── album.png │ │ │ │ ├── zoom.png │ │ │ │ ├── camera.png │ │ │ │ ├── resize_frame.png │ │ │ │ └── viewfinder_overlay.png │ │ │ ├── item │ │ │ │ ├── album.png │ │ │ │ ├── camera.png │ │ │ │ ├── image.png │ │ │ │ └── image_frame.png │ │ │ └── images │ │ │ │ ├── frame_back.png │ │ │ │ ├── frame_side.png │ │ │ │ ├── empty_image.png │ │ │ │ └── default_image.png │ │ │ ├── items │ │ │ ├── album.json │ │ │ ├── camera.json │ │ │ ├── image_frame.json │ │ │ └── image.json │ │ │ ├── models │ │ │ └── item │ │ │ │ ├── album.json │ │ │ │ ├── camera.json │ │ │ │ ├── image_frame.json │ │ │ │ └── image.json │ │ │ ├── sounds.json │ │ │ ├── lang │ │ │ ├── tr_tr.json │ │ │ ├── zh_cn.json │ │ │ ├── zh_tw.json │ │ │ ├── ko_kr.json │ │ │ ├── es_ar.json │ │ │ ├── pt_br.json │ │ │ ├── no_no.json │ │ │ ├── de_de.json │ │ │ ├── en_us.json │ │ │ ├── fr_fr.json │ │ │ ├── cs_cz.json │ │ │ ├── uk_ua.json │ │ │ ├── es_es.json │ │ │ └── ru_ru.json │ │ │ └── post_effect │ │ │ ├── inverted.json │ │ │ ├── overexposed.json │ │ │ ├── desaturated.json │ │ │ ├── oversaturated.json │ │ │ ├── sepia.json │ │ │ ├── black_and_white.json │ │ │ └── blurry.json │ ├── pack.mcmeta │ ├── camera.mixins.json │ └── META-INF │ │ └── neoforge.mods.toml │ └── java │ └── de │ └── maxhenkel │ └── camera │ ├── gui │ ├── AlbumSlot.java │ ├── DummyContainer.java │ ├── AlbumInventoryContainer.java │ ├── AlbumInventoryScreen.java │ ├── AlbumContainer.java │ ├── LecternAlbumScreen.java │ ├── ImageScreen.java │ ├── AlbumScreen.java │ ├── CameraScreen.java │ └── ResizeFrameScreen.java │ ├── integration │ ├── waila │ │ ├── PluginCamera.java │ │ └── HUDHandlerImageFrame.java │ └── jei │ │ ├── NoJEIGuiProperties.java │ │ ├── JEIPlugin.java │ │ └── ImageCloneExtension.java │ ├── ClientImageUploadManager.java │ ├── entities │ ├── ImageEntityRenderState.java │ └── ImageRenderer.java │ ├── CreativeTabEvents.java │ ├── ModSounds.java │ ├── ImageTaker.java │ ├── net │ ├── MessageTakeImage.java │ ├── MessageTakeBook.java │ ├── MessageImageUnavailable.java │ ├── MessageAlbumPage.java │ ├── MessageDisableCameraMode.java │ ├── MessageUploadCustomImage.java │ ├── MessageSetShader.java │ ├── MessageRequestImage.java │ ├── MessagePartialImage.java │ ├── MessageImage.java │ ├── MessageRequestUploadCustomImage.java │ ├── MessageResizeFrame.java │ └── PacketManager.java │ ├── Shaders.java │ ├── ClientConfig.java │ ├── items │ ├── ImageFrameItem.java │ ├── ImageItem.java │ ├── render │ │ └── ImageSpecialRenderer.java │ ├── AlbumItem.java │ └── CameraItem.java │ ├── ImageProcessor.java │ ├── ServerConfig.java │ ├── inventory │ └── AlbumInventory.java │ ├── ServerEvents.java │ ├── CameraClientMod.java │ ├── TextureCache.java │ ├── mixins │ └── LecternTileEntityMixin.java │ ├── ImageCloningRecipe.java │ ├── ClientEvents.java │ └── CameraMod.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ ├── translation.yml │ └── bug_report.yml ├── settings.gradle ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── readme.md └── gradlew /changelog.md: -------------------------------------------------------------------------------- 1 | - Updated to 1.21.11 2 | -------------------------------------------------------------------------------- /src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/icon.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/data/camera/tags/item/image_paper.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | "minecraft:paper" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/main/resources/data/minecraft/tags/item/lectern_books.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | "camera:album" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/sounds/take_image.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/sounds/take_image.ogg -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/gui/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/gui/album.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/gui/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/gui/zoom.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/items/album.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "minecraft:model", 4 | "model": "camera:item/album" 5 | } 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/items/camera.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "minecraft:model", 4 | "model": "camera:item/camera" 5 | } 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/gui/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/gui/camera.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/item/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/item/album.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/item/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/item/camera.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/item/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/item/image.png -------------------------------------------------------------------------------- /src/main/resources/data/camera/recipe/image_cloning.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "camera:image_cloning", 3 | "image": "camera:image", 4 | "paper": "#camera:image_paper" 5 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/items/image_frame.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "minecraft:model", 4 | "model": "camera:item/image_frame" 5 | } 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/models/item/album.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "item/generated", 3 | "textures": { 4 | "layer0": "camera:item/album" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/gui/resize_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/gui/resize_frame.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/images/frame_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/images/frame_back.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/images/frame_side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/images/frame_side.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/item/image_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/item/image_frame.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/models/item/camera.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "item/generated", 3 | "textures": { 4 | "layer0": "camera:item/camera" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/images/empty_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/images/empty_image.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/gui/viewfinder_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/gui/viewfinder_overlay.png -------------------------------------------------------------------------------- /src/main/resources/assets/camera/textures/images/default_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/camera/HEAD/src/main/resources/assets/camera/textures/images/default_image.png -------------------------------------------------------------------------------- /src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "description": "camera resources", 4 | "min_format": [65, 0], 5 | "max_format": [999, 0] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/models/item/image_frame.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "item/generated", 3 | "textures": { 4 | "layer0": "camera:item/image_frame" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: FAQ 4 | about: Frequently asked questions 5 | url: https://modrepo.de/minecraft/camera/faq 6 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/items/image.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "minecraft:special", 4 | "base": "camera:item/image", 5 | "model": { 6 | "type": "camera:image" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/sounds.json: -------------------------------------------------------------------------------- 1 | { 2 | "take_image": { 3 | "category": "master", 4 | "subtitle": "sound.camera.take_image", 5 | "sounds": [ 6 | { 7 | "name": "camera:take_image" 8 | } 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/resources/data/camera/recipe/album.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crafting_shapeless", 3 | "ingredients": [ 4 | "#c:strings", 5 | "#c:leathers", 6 | "minecraft:paper", 7 | "minecraft:paper", 8 | "minecraft:paper" 9 | ], 10 | "result": { 11 | "id": "camera:album", 12 | "count": 1 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/resources/camera.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "de.maxhenkel.camera.mixins", 4 | "compatibilityLevel": "JAVA_17", 5 | "refmap": "camera.refmap.json", 6 | "minVersion": "0.8", 7 | "mixins": [ 8 | "LecternTileEntityMixin" 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { url = 'https://maven.neoforged.net/releases' } 5 | maven { url = 'https://maven.maxhenkel.de/repository/public' } 6 | } 7 | } 8 | 9 | plugins { 10 | id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | 16 | # gradle 17 | build 18 | .gradle 19 | 20 | # other 21 | eclipse 22 | run 23 | logs 24 | 25 | curseforge_api_key.txt 26 | mod_update_api_key.txt 27 | modrinth_token.txt 28 | runs 29 | -------------------------------------------------------------------------------- /src/main/resources/data/camera/recipe/image_frame.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crafting_shaped", 3 | "pattern": [ 4 | "SFS", 5 | "SLS", 6 | "SSS" 7 | ], 8 | "key": { 9 | "S": "#c:rods/wooden", 10 | "F": "#c:strings", 11 | "L": "#c:leathers" 12 | }, 13 | "result": { 14 | "id": "camera:image_frame", 15 | "count": 1 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/resources/data/camera/recipe/camera.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crafting_shaped", 3 | "pattern": [ 4 | "IRS", 5 | "IGI", 6 | "III" 7 | ], 8 | "key": { 9 | "I": "#c:ingots/iron", 10 | "R": "#c:dusts/redstone", 11 | "S": "minecraft:stone_button", 12 | "G": "#c:glass_panes/colorless" 13 | }, 14 | "result": { 15 | "id": "camera:camera", 16 | "count": 1 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/AlbumSlot.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.items.ImageItem; 4 | import net.minecraft.world.Container; 5 | import net.minecraft.world.inventory.Slot; 6 | import net.minecraft.world.item.ItemStack; 7 | 8 | public class AlbumSlot extends Slot { 9 | 10 | public AlbumSlot(Container inventory, int index, int x, int y) { 11 | super(inventory, index, x, y); 12 | } 13 | 14 | @Override 15 | public boolean mayPlace(ItemStack stack) { 16 | return stack.getItem() instanceof ImageItem; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/integration/waila/PluginCamera.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.integration.waila; 2 | 3 | import de.maxhenkel.camera.entities.ImageEntity; 4 | import snownee.jade.api.IWailaClientRegistration; 5 | import snownee.jade.api.IWailaPlugin; 6 | import snownee.jade.api.WailaPlugin; 7 | 8 | @WailaPlugin 9 | public class PluginCamera implements IWailaPlugin { 10 | 11 | @Override 12 | public void registerClient(IWailaClientRegistration registration) { 13 | registration.registerEntityComponent(HUDHandlerImageFrame.INSTANCE, ImageEntity.class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ClientImageUploadManager.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.UUID; 7 | 8 | public class ClientImageUploadManager { 9 | 10 | private static Map images = new HashMap<>(); 11 | 12 | public static void addImage(UUID uuid, BufferedImage image) { 13 | images.put(uuid, image); 14 | } 15 | 16 | public static BufferedImage getAndRemoveImage(UUID uuid) { 17 | return images.remove(uuid); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/DummyContainer.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import net.minecraft.world.entity.player.Player; 4 | import net.minecraft.world.inventory.AbstractContainerMenu; 5 | import net.minecraft.world.item.ItemStack; 6 | 7 | public class DummyContainer extends AbstractContainerMenu { 8 | 9 | protected DummyContainer() { 10 | super(null, 0); 11 | } 12 | 13 | @Override 14 | public ItemStack quickMoveStack(Player player, int index) { 15 | return ItemStack.EMPTY; 16 | } 17 | 18 | @Override 19 | public boolean stillValid(Player playerIn) { 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/entities/ImageEntityRenderState.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.entities; 2 | 3 | import net.minecraft.client.renderer.entity.state.EntityRenderState; 4 | import net.minecraft.core.Direction; 5 | import net.minecraft.resources.Identifier; 6 | import net.minecraft.world.phys.AABB; 7 | 8 | import java.util.UUID; 9 | 10 | public class ImageEntityRenderState extends EntityRenderState { 11 | 12 | public int light; 13 | public int frameWidth; 14 | public int frameHeight; 15 | public Direction facing; 16 | public ImageState imageState; 17 | public UUID imageEntityUUID; 18 | public AABB imageBoundingBox; 19 | 20 | public record ImageState(UUID imageId, float imageRatio, Identifier resourceLocation) { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/CreativeTabEvents.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import net.minecraft.world.item.CreativeModeTabs; 4 | import net.minecraft.world.item.ItemStack; 5 | import net.neoforged.bus.api.SubscribeEvent; 6 | import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; 7 | 8 | public class CreativeTabEvents { 9 | 10 | @SubscribeEvent 11 | public static void onCreativeModeTabBuildContents(BuildCreativeModeTabContentsEvent event) { 12 | if (event.getTabKey().equals(CreativeModeTabs.TOOLS_AND_UTILITIES)) { 13 | event.accept(new ItemStack(CameraMod.CAMERA.get())); 14 | event.accept(new ItemStack(CameraMod.ALBUM.get())); 15 | event.accept(new ItemStack(CameraMod.FRAME_ITEM.get())); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ModSounds.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import net.minecraft.core.registries.BuiltInRegistries; 4 | import net.minecraft.resources.Identifier; 5 | import net.minecraft.sounds.SoundEvent; 6 | import net.neoforged.neoforge.registries.DeferredHolder; 7 | import net.neoforged.neoforge.registries.DeferredRegister; 8 | 9 | public class ModSounds { 10 | 11 | public static final DeferredRegister SOUND_REGISTER = DeferredRegister.create(BuiltInRegistries.SOUND_EVENT, CameraMod.MODID); 12 | 13 | //https://www.soundjay.com/mechanical/sounds/camera-shutter-click-01.mp3 14 | public static final DeferredHolder TAKE_IMAGE = SOUND_REGISTER.register("take_image", () -> SoundEvent.createVariableRangeEvent(Identifier.fromNamespaceAndPath(CameraMod.MODID, "take_image"))); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/translation.yml: -------------------------------------------------------------------------------- 1 | name: Translation 2 | description: Submit a translation for this project 3 | labels: [translation] 4 | assignees: henkelmax 5 | body: 6 | - type: textarea 7 | id: notes 8 | attributes: 9 | label: Additional notes 10 | description: Additional information. 11 | validations: 12 | required: false 13 | - type: input 14 | id: locale_code 15 | attributes: 16 | label: Locale code 17 | description: The Minecraft locale code (See [this](https://minecraft.wiki/w/Language#Languages) for more information). 18 | placeholder: en_us 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: translation 23 | attributes: 24 | label: Translation json 25 | description: The contents of your translation file. 26 | render: json 27 | placeholder: | 28 | { 29 | "translation.key": "Translated value" 30 | } 31 | validations: 32 | required: true 33 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | org.gradle.daemon=false 3 | 4 | java_version=21 5 | java_toolchain_version=21 6 | 7 | neogradle.subsystems.parchment.minecraftVersion=1.21.10 8 | neogradle.subsystems.parchment.mappingsVersion=2025.10.12 9 | 10 | mod_loader=neoforge 11 | minecraft_version=1.21.11 12 | neoforge_version=21.11.0-beta 13 | neoforge_dependency=[21.11.0-beta,) 14 | corelib_version=2.1.12 15 | 16 | jei_version=21.3.2.22 17 | jade_version=19.0.3+neoforge 18 | 19 | # Mod information 20 | mod_version=1.21.11-1.1.8 21 | mod_id=camera 22 | mod_display_name=Camera Mod 23 | 24 | # Script configuration 25 | use_mixins=true 26 | 27 | # Project upload 28 | curseforge_upload_id=289310 29 | modrinth_upload_id=oiuNWinn 30 | upload_release_type=beta 31 | upload_recommended=true 32 | 33 | curseforge_upload_optional_dependencies=jei, jade 34 | modrinth_upload_optional_dependencies=jei, jade 35 | 36 | # Gradle plugins 37 | mod_gradle_script_version=1.0.47 38 | neogradle_version=[7.1.11,7.2) 39 | mod_update_version=2.0.0 40 | cursegradle_version=1.5.0 41 | shadow_version=9.2.2 42 | minotaur_version=2.+ 43 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/models/item/image.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "builtin/entity", 3 | "display": { 4 | "gui": { 5 | "rotation": [ 20, 20, 0 ], 6 | "translation": [ 2.2, -2.2, 0], 7 | "scale":[ 0.9, 0.9, 0.9 ] 8 | }, 9 | "ground": { 10 | "rotation": [ 0, 0, 0 ], 11 | "translation": [ 0, 0, 4], 12 | "scale":[ 0.5, 0.5, 0.5 ] 13 | }, 14 | "head": { 15 | "rotation": [ 0, 180, 0 ], 16 | "translation": [ 0, 0, 0], 17 | "scale":[ 1, 1, 1] 18 | }, 19 | "fixed": { 20 | "rotation": [ 0, 180, 0 ], 21 | "translation": [ 0, 0, 0], 22 | "scale":[ 0.5, 0.5, 0.5 ] 23 | }, 24 | "thirdperson_righthand": { 25 | "rotation": [ 0, 0, 0 ], 26 | "translation": [ 0, 2, 4], 27 | "scale": [ 0.5, 0.5, 0.5 ] 28 | }, 29 | "firstperson_righthand": { 30 | "rotation": [ -30, 315, -25 ], 31 | "translation": [ -2, 4, 0], 32 | "scale": [ 1, 1, 1 ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | modLoader = "javafml" 2 | loaderVersion = "*" 3 | license = "All rights reserved" 4 | issueTrackerURL = "https://github.com/henkelmax/camera/issues" 5 | [[mods]] 6 | modId = "camera" 7 | version = "${mod_version}" 8 | displayName = "Camera Mod" 9 | updateJSONURL = "https://update.maxhenkel.de/neoforge/camera" 10 | displayURL = "https://www.curseforge.com/minecraft/mc-mods/camera-mod" 11 | logoFile = "icon.png" 12 | authors = "Max Henkel" 13 | description = '''This mod adds a working camera''' 14 | [[mixins]] 15 | config = "camera.mixins.json" 16 | [[dependencies.camera]] 17 | modId = "neoforge" 18 | type = "required" 19 | versionRange = "${neoforge_dependency}" 20 | ordering = "NONE" 21 | side = "BOTH" 22 | [[dependencies.camera]] 23 | modId = "minecraft" 24 | type = "required" 25 | versionRange = "[${minecraft_version}]" 26 | ordering = "NONE" 27 | side = "BOTH" 28 | [[dependencies.camera]] 29 | modId = "jei" 30 | type = "optional" 31 | versionRange = "*" 32 | ordering = "NONE" 33 | side = "BOTH" 34 | [[dependencies.camera]] 35 | modId = "jade" 36 | type = "optional" 37 | versionRange = "*" 38 | ordering = "NONE" 39 | side = "BOTH" -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/AlbumInventoryContainer.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.corelib.inventory.ContainerBase; 5 | import net.minecraft.world.Container; 6 | import net.minecraft.world.SimpleContainer; 7 | 8 | public class AlbumInventoryContainer extends ContainerBase { 9 | 10 | public AlbumInventoryContainer(int id, Container playerInventory, Container albumInventory) { 11 | super(CameraMod.ALBUM_INVENTORY_CONTAINER.get(), id, playerInventory, albumInventory); 12 | 13 | for (int x = 0; x < 6; x++) { 14 | for (int y = 0; y < 9; y++) { 15 | addSlot(new AlbumSlot(albumInventory, y + x * 9, 8 + y * 18, 18 + x * 18)); 16 | } 17 | } 18 | 19 | addPlayerInventorySlots(); 20 | } 21 | 22 | public AlbumInventoryContainer(int id, Container playerInventory) { 23 | this(id, playerInventory, new SimpleContainer(54)); 24 | } 25 | 26 | @Override 27 | public int getInventorySize() { 28 | return 54; 29 | } 30 | 31 | @Override 32 | public int getInvOffset() { 33 | return 56; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/tr_tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.camera.camera": "Kamera", 3 | "item.camera.image": "Fotoğraf", 4 | "item.camera.image_frame": "Fotoğraf Çerçevesi", 5 | "item.camera.album": "Albüm", 6 | "message.no_consumable": "Hayır veya envanterde yeteri kadar yok", 7 | "message.image_cooldown": "Kamera soğumada", 8 | "tooltip.image_time": "Şu saatte çekildi: %s", 9 | "tooltip.image_owner": "Çeken: %s", 10 | "button.camera.prev": "Önceki", 11 | "button.camera.next": "Sonraki", 12 | "gui.camera.choose_filter": "Filtre Seçin", 13 | "gui.camera.title": "Kamera", 14 | "gui.frame.resize": "Çerçeveyi Yeniden Boyutlandır", 15 | "gui.album.title": "Albüm", 16 | "gui.image.title": "Fotoğraf", 17 | "shader.none": "Yok", 18 | "shader.black_and_white": "Siyah Beyaz", 19 | "shader.sepia": "Sepya", 20 | "shader.desaturated": "Desatüre", 21 | "shader.overexposed": "Sürekspoze", 22 | "shader.oversaturated": "Aşırı Doygun", 23 | "shader.blurry": "Bulanık", 24 | "shader.inverted": "Ters Çevrilmiş", 25 | "button.frame.left": "Sol", 26 | "button.frame.right": "Sağ", 27 | "button.frame.up": "Yukarı", 28 | "button.frame.down": "Aşağı", 29 | "key.next_image": "Sonraki Fotoğraf", 30 | "key.previous_image": "Önceki Fotoğraf" 31 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/inverted.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/invert", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main" 13 | } 14 | ], 15 | "output": "swap", 16 | "uniforms": { 17 | "InvertConfig": [ 18 | { 19 | "name": "InverseAmount", 20 | "type": "float", 21 | "value": 0.8 22 | } 23 | ] 24 | } 25 | }, 26 | { 27 | "vertex_shader": "minecraft:core/screenquad", 28 | "fragment_shader": "minecraft:post/blit", 29 | "inputs": [ 30 | { 31 | "sampler_name": "In", 32 | "target": "swap" 33 | } 34 | ], 35 | "uniforms": { 36 | "BlitConfig": [ 37 | { 38 | "name": "ColorModulate", 39 | "type": "vec4", 40 | "value": [ 41 | 1.0, 42 | 1.0, 43 | 1.0, 44 | 1.0 45 | ] 46 | } 47 | ] 48 | }, 49 | "output": "minecraft:main" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/integration/jei/NoJEIGuiProperties.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.integration.jei; 2 | 3 | import mezz.jei.api.gui.handlers.IGuiProperties; 4 | import net.minecraft.client.gui.screens.Screen; 5 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 6 | 7 | public class NoJEIGuiProperties implements IGuiProperties { 8 | 9 | private final AbstractContainerScreen containerScreen; 10 | 11 | public NoJEIGuiProperties(AbstractContainerScreen containerScreen) { 12 | this.containerScreen = containerScreen; 13 | } 14 | 15 | @Override 16 | public Class screenClass() { 17 | return containerScreen.getClass(); 18 | } 19 | 20 | @Override 21 | public int guiLeft() { 22 | return containerScreen.width; 23 | } 24 | 25 | @Override 26 | public int guiTop() { 27 | return containerScreen.height; 28 | } 29 | 30 | @Override 31 | public int guiXSize() { 32 | return containerScreen.width; 33 | } 34 | 35 | @Override 36 | public int guiYSize() { 37 | return containerScreen.height; 38 | } 39 | 40 | @Override 41 | public int screenWidth() { 42 | return containerScreen.width; 43 | } 44 | 45 | @Override 46 | public int screenHeight() { 47 | return containerScreen.height; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/AlbumInventoryScreen.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.corelib.inventory.ScreenBase; 5 | import net.minecraft.client.gui.GuiGraphics; 6 | import net.minecraft.network.chat.Component; 7 | import net.minecraft.resources.Identifier; 8 | import net.minecraft.world.entity.player.Inventory; 9 | 10 | public class AlbumInventoryScreen extends ScreenBase { 11 | 12 | public static final Identifier DEFAULT_IMAGE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/gui/album.png"); 13 | 14 | private Inventory playerInventory; 15 | 16 | public AlbumInventoryScreen(AlbumInventoryContainer albumInventory, Inventory playerInventory, Component name) { 17 | super(DEFAULT_IMAGE, albumInventory, playerInventory, name); 18 | 19 | this.playerInventory = playerInventory; 20 | imageWidth = 176; 21 | imageHeight = 222; 22 | } 23 | 24 | @Override 25 | protected void renderLabels(GuiGraphics guiGraphics, int x, int y) { 26 | super.renderLabels(guiGraphics, x, y); 27 | guiGraphics.drawString(font, getTitle().getVisualOrderText(), 8, 6, FONT_COLOR, false); 28 | guiGraphics.drawString(font, playerInventory.getDisplayName().getVisualOrderText(), 8, imageHeight - 96 + 2, FONT_COLOR, false); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ImageTaker.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import net.minecraft.client.Minecraft; 4 | import net.minecraft.client.Screenshot; 5 | import net.neoforged.api.distmarker.Dist; 6 | import net.neoforged.bus.api.SubscribeEvent; 7 | import net.neoforged.fml.common.EventBusSubscriber; 8 | import net.neoforged.neoforge.client.event.RenderFrameEvent; 9 | 10 | import java.util.UUID; 11 | 12 | @EventBusSubscriber(modid = CameraMod.MODID, value = Dist.CLIENT) 13 | public class ImageTaker { 14 | 15 | private static boolean takeScreenshot; 16 | private static UUID uuid; 17 | private static boolean hide; 18 | 19 | public static void takeScreenshot(UUID id) { 20 | if (takeScreenshot && id.equals(uuid)) { 21 | return; 22 | } 23 | Minecraft mc = Minecraft.getInstance(); 24 | 25 | hide = mc.options.hideGui; 26 | mc.options.hideGui = true; 27 | 28 | takeScreenshot = true; 29 | uuid = id; 30 | mc.setScreen(null); 31 | } 32 | 33 | @SubscribeEvent 34 | public static void onRenderTickEnd(RenderFrameEvent.Post event) { 35 | if (!takeScreenshot) { 36 | return; 37 | } 38 | 39 | Minecraft mc = Minecraft.getInstance(); 40 | 41 | Screenshot.takeScreenshot(mc.getMainRenderTarget(), image -> { 42 | mc.options.hideGui = hide; 43 | takeScreenshot = false; 44 | 45 | ImageProcessor.sendScreenshotThreaded(uuid, image); 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "相框", 3 | "item.camera.camera": "相机", 4 | "item.camera.image": "相片", 5 | "item.camera.image_frame": "相框", 6 | "item.camera.album": "相册", 7 | "message.no_consumable": "背包中没有足够的", 8 | "message.image_cooldown": "相机还在冷却中", 9 | "message.upload_error": "相片上传错误:%s", 10 | "tooltip.image_time": "摄制时间 %s", 11 | "tooltip.image_owner": "摄制者 %s", 12 | "tooltip.image_frame_empty": "没有相片", 13 | "button.camera.prev": "上一个", 14 | "button.camera.next": "下一个", 15 | "button.camera.upload": "上传", 16 | "gui.camera.choose_filter": "选择一种滤镜", 17 | "gui.camera.title": "相机", 18 | "gui.frame.resize": "调整相框大小", 19 | "gui.frame.resize_description": "按住 Shift 键以缩小", 20 | "tooltip.visibility": "切换可见性", 21 | "tooltip.visibility_short": "V", 22 | "gui.album.title": "相册", 23 | "gui.image.title": "相片", 24 | "shader.none": "无", 25 | "shader.black_and_white": "黑白", 26 | "shader.sepia": "褐色", 27 | "shader.desaturated": "低饱和度", 28 | "shader.overexposed": "过度曝光", 29 | "shader.oversaturated": "高饱和度", 30 | "shader.blurry": "模糊", 31 | "shader.inverted": "倒转", 32 | "button.frame.left": "左", 33 | "button.frame.right": "右", 34 | "button.frame.up": "上", 35 | "button.frame.down": "下", 36 | "key.next_image": "新相片", 37 | "key.previous_image": "上一张相片", 38 | "filetype.images": "相片", 39 | "title.choose_image": "选择相片", 40 | "sound.camera.take_image": "相机拍摄了一张相片" 41 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/integration/jei/JEIPlugin.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.integration.jei; 2 | 3 | import de.maxhenkel.camera.ImageCloningRecipe; 4 | import de.maxhenkel.camera.CameraMod; 5 | import de.maxhenkel.camera.gui.*; 6 | import mezz.jei.api.IModPlugin; 7 | import mezz.jei.api.JeiPlugin; 8 | import mezz.jei.api.recipe.category.extensions.vanilla.crafting.IExtendableCraftingRecipeCategory; 9 | import mezz.jei.api.registration.*; 10 | import net.minecraft.resources.Identifier; 11 | 12 | @JeiPlugin 13 | public class JEIPlugin implements IModPlugin { 14 | 15 | @Override 16 | public Identifier getPluginUid() { 17 | return Identifier.fromNamespaceAndPath(CameraMod.MODID, "camera"); 18 | } 19 | 20 | @Override 21 | public void registerGuiHandlers(IGuiHandlerRegistration registration) { 22 | registration.addGuiScreenHandler(ImageScreen.class, NoJEIGuiProperties::new); 23 | registration.addGuiScreenHandler(AlbumScreen.class, NoJEIGuiProperties::new); 24 | registration.addGuiScreenHandler(LecternAlbumScreen.class, NoJEIGuiProperties::new); 25 | registration.addGuiScreenHandler(ResizeFrameScreen.class, NoJEIGuiProperties::new); 26 | registration.addGuiScreenHandler(CameraScreen.class, NoJEIGuiProperties::new); 27 | } 28 | 29 | @Override 30 | public void registerVanillaCategoryExtensions(IVanillaCategoryExtensionRegistration registration) { 31 | IExtendableCraftingRecipeCategory craftingCategory = registration.getCraftingCategory(); 32 | craftingCategory.addExtension(ImageCloningRecipe.class, new ImageCloneExtension<>()); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/zh_tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "畫框", 3 | "item.camera.camera": "相機", 4 | "item.camera.image": "相片", 5 | "item.camera.image_frame": "畫框", 6 | "item.camera.album": "相簿", 7 | "message.no_consumable": "物品欄中沒有材料或材料不足", 8 | "message.image_cooldown": "相機正在冷卻", 9 | "message.upload_error": "相片上傳錯誤:%s", 10 | "tooltip.image_time": "拍攝於 %s", 11 | "tooltip.image_owner": "拍攝者:%s", 12 | "tooltip.image_frame_empty": "沒有相片", 13 | "button.camera.prev": "上一張", 14 | "button.camera.next": "下一張", 15 | "button.camera.upload": "上傳", 16 | "gui.camera.choose_filter": "選擇濾鏡", 17 | "gui.camera.upload_image": "上傳相片", 18 | "gui.camera.title": "相機", 19 | "gui.frame.resize": "調整畫框大小", 20 | "gui.frame.resize_description": "按住 SHIFT 縮小", 21 | "tooltip.visibility": "切換可見度", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "相簿", 24 | "gui.image.title": "相片", 25 | "shader.none": "無", 26 | "shader.black_and_white": "黑白", 27 | "shader.sepia": "懷舊", 28 | "shader.desaturated": "消色", 29 | "shader.overexposed": "過曝", 30 | "shader.oversaturated": "過飽和", 31 | "shader.blurry": "模糊", 32 | "shader.inverted": "負片", 33 | "button.frame.left": "左", 34 | "button.frame.right": "右", 35 | "button.frame.up": "上", 36 | "button.frame.down": "下", 37 | "key.next_image": "下一張相片", 38 | "key.previous_image": "上一張相片", 39 | "filetype.images": "相片", 40 | "title.choose_image": "選擇相片", 41 | "sound.camera.take_image": "相機拍照", 42 | "jei.camera.take_image": "拍攝相片", 43 | "jei.camera.tooltip.take_image": "拍攝相片時,會從你的物品欄中消耗 %sx %s", 44 | "config.jade.plugin_camera.image_frame": "畫框" 45 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/ko_kr.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "이미지 액자", 3 | "item.camera.camera": "카메라", 4 | "item.camera.image": "이미지", 5 | "item.camera.image_frame": "이미지 액자", 6 | "item.camera.album": "앨범", 7 | "message.no_consumable": "재료가 보관함에 없거나 부족합니다.", 8 | "message.image_cooldown": "카메라가 쿨타임 중입니다.", 9 | "message.upload_error": "이미지 업로드 에러: %s", 10 | "tooltip.image_time": "%s에 찍힘", 11 | "tooltip.image_owner": "%s가 찍음", 12 | "tooltip.image_frame_empty": "이미지 없음", 13 | "button.camera.prev": "이전", 14 | "button.camera.next": "다음", 15 | "button.camera.upload": "업로드", 16 | "gui.camera.choose_filter": "필터 고르기", 17 | "gui.camera.upload_image": "이미지 업로드", 18 | "gui.camera.title": "카메라", 19 | "gui.frame.resize": "액자 크기 재설정", 20 | "gui.frame.resize_description": "쉬프트로 줄입니다.", 21 | "tooltip.visibility": "가시성 전환", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "앨범", 24 | "gui.image.title": "이미지", 25 | "shader.none": "없음", 26 | "shader.black_and_white": "흑백", 27 | "shader.sepia": "세피아", 28 | "shader.desaturated": "저채도", 29 | "shader.overexposed": "노출 과자", 30 | "shader.oversaturated": "고채도", 31 | "shader.blurry": "흐릿한", 32 | "shader.inverted": "반전", 33 | "button.frame.left": "왼쪽", 34 | "button.frame.right": "오른쪽", 35 | "button.frame.up": "위", 36 | "button.frame.down": "아래", 37 | "key.next_image": "다음 이미지", 38 | "key.previous_image": "이전 이미지", 39 | "filetype.images": "이미지", 40 | "title.choose_image": "이미지 선택", 41 | "sound.camera.take_image": "이미지를 찍는 카메라", 42 | "jei.camera.take_image": "이미지 찍기", 43 | "jei.camera.tooltip.take_image": "사진을 찍을 때 %sx %s 사용" 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageTakeImage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.ImageTaker; 4 | import de.maxhenkel.camera.CameraMod; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.protocol.PacketFlow; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.neoforged.neoforge.network.handling.IPayloadContext; 11 | 12 | import java.util.UUID; 13 | 14 | public class MessageTakeImage implements Message { 15 | 16 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "take_image")); 17 | 18 | private UUID uuid; 19 | 20 | public MessageTakeImage() { 21 | 22 | } 23 | 24 | public MessageTakeImage(UUID uuid) { 25 | this.uuid = uuid; 26 | } 27 | 28 | @Override 29 | public PacketFlow getExecutingSide() { 30 | return PacketFlow.CLIENTBOUND; 31 | } 32 | 33 | @Override 34 | public void executeClientSide(IPayloadContext context) { 35 | ImageTaker.takeScreenshot(uuid); 36 | } 37 | 38 | @Override 39 | public MessageTakeImage fromBytes(RegistryFriendlyByteBuf buf) { 40 | uuid = buf.readUUID(); 41 | return this; 42 | } 43 | 44 | @Override 45 | public void toBytes(RegistryFriendlyByteBuf buf) { 46 | buf.writeUUID(uuid); 47 | } 48 | 49 | @Override 50 | public Type type() { 51 | return TYPE; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageTakeBook.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.gui.AlbumContainer; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.protocol.PacketFlow; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.server.level.ServerPlayer; 11 | import net.neoforged.neoforge.network.handling.IPayloadContext; 12 | 13 | public class MessageTakeBook implements Message { 14 | 15 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "take_book")); 16 | 17 | public MessageTakeBook() { 18 | 19 | } 20 | 21 | @Override 22 | public PacketFlow getExecutingSide() { 23 | return PacketFlow.SERVERBOUND; 24 | } 25 | 26 | @Override 27 | public void executeServerSide(IPayloadContext context) { 28 | if (!(context.player() instanceof ServerPlayer sender)) { 29 | return; 30 | } 31 | if (sender.containerMenu instanceof AlbumContainer container) { 32 | container.takeBook(sender); 33 | } 34 | } 35 | 36 | @Override 37 | public MessageTakeBook fromBytes(RegistryFriendlyByteBuf buf) { 38 | return this; 39 | } 40 | 41 | @Override 42 | public void toBytes(RegistryFriendlyByteBuf buf) { 43 | } 44 | 45 | @Override 46 | public Type type() { 47 | return TYPE; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/Shaders.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import net.minecraft.resources.Identifier; 4 | 5 | import java.util.*; 6 | 7 | public class Shaders { 8 | 9 | public static Identifier BLACK_AND_WHITE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "black_and_white"); 10 | public static Identifier SEPIA = Identifier.fromNamespaceAndPath(CameraMod.MODID, "sepia"); 11 | public static Identifier DESATURATED = Identifier.fromNamespaceAndPath(CameraMod.MODID, "desaturated"); 12 | public static Identifier OVEREXPOSED = Identifier.fromNamespaceAndPath(CameraMod.MODID, "overexposed"); 13 | public static Identifier OVERSATURATED = Identifier.fromNamespaceAndPath(CameraMod.MODID, "oversaturated"); 14 | public static Identifier BLURRY = Identifier.fromNamespaceAndPath(CameraMod.MODID, "blurry"); 15 | public static Identifier INVERTED = Identifier.fromNamespaceAndPath(CameraMod.MODID, "inverted"); 16 | 17 | private static Map shaders; 18 | public static final List SHADER_LIST; 19 | 20 | static { 21 | shaders = new HashMap<>(); 22 | shaders.put("none", null); 23 | shaders.put("black_and_white", BLACK_AND_WHITE); 24 | shaders.put("sepia", SEPIA); 25 | shaders.put("desaturated", DESATURATED); 26 | shaders.put("overexposed", OVEREXPOSED); 27 | shaders.put("oversaturated", OVERSATURATED); 28 | shaders.put("blurry", BLURRY); 29 | shaders.put("inverted", INVERTED); 30 | SHADER_LIST = Collections.unmodifiableList(new ArrayList<>(shaders.keySet())); 31 | } 32 | 33 | public static Identifier getShader(String name) { 34 | return shaders.get(name); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/es_ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Marco de imagen", 3 | "item.camera.camera": "Cámara", 4 | "item.camera.image": "Imagen", 5 | "item.camera.image_frame": "Marco de imagen", 6 | "item.camera.album": "Álbum", 7 | "message.no_consumable": "No hay o no hay suficiente en el inventario", 8 | "message.image_cooldown": "La cámara está en enfriamiento", 9 | "message.upload_error": "Error de carga de imagen: %s", 10 | "tooltip.image_time": "Tomada el %s", 11 | "tooltip.image_owner": "Tomada por %s", 12 | "tooltip.image_frame_empty": "Sin imágen", 13 | "button.camera.prev": "Anterior", 14 | "button.camera.next": "Siguiente", 15 | "button.camera.upload": "Subir", 16 | "gui.camera.choose_filter": "Elegir un filtro", 17 | "gui.camera.title": "Cámara", 18 | "gui.frame.resize": "Redimensionar marco", 19 | "gui.frame.resize_description": "Mantener SHIFT para achicar", 20 | "tooltip.visibility": "Alternar visibilidad", 21 | "tooltip.visibility_short": "V", 22 | "gui.album.title": "Álbum", 23 | "gui.image.title": "Imagen", 24 | "shader.none": "Ninguno", 25 | "shader.black_and_white": "Blanco y negro", 26 | "shader.sepia": "Sepia", 27 | "shader.desaturated": "Desaturado", 28 | "shader.overexposed": "Sobreexpuesto", 29 | "shader.oversaturated": "Sobresaturado", 30 | "shader.blurry": "Borroso", 31 | "shader.inverted": "Invertido", 32 | "button.frame.left": "Izquierda", 33 | "button.frame.right": "Derecha", 34 | "button.frame.up": "Arriba", 35 | "button.frame.down": "Abajo", 36 | "key.next_image": "Próxima imagen", 37 | "key.previous_image": "Imagen anterior", 38 | "filetype.images": "Imagenes", 39 | "title.choose_image": "Elegir imagen", 40 | "sound.camera.take_image": "Cámara tomando una imagen" 41 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/overexposed.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/color_convolve", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main" 13 | } 14 | ], 15 | "output": "swap", 16 | "uniforms": { 17 | "ColorConfig": [ 18 | { 19 | "name": "RedMatrix", 20 | "type": "vec3", 21 | "value": [ 22 | 1.5, 23 | 0.5, 24 | 0.5 25 | ] 26 | }, 27 | { 28 | "name": "GreenMatrix", 29 | "type": "vec3", 30 | "value": [ 31 | 0.5, 32 | 1.5, 33 | 0.5 34 | ] 35 | }, 36 | { 37 | "name": "BlueMatrix", 38 | "type": "vec3", 39 | "value": [ 40 | 0.5, 41 | 0.5, 42 | 1.5 43 | ] 44 | } 45 | ] 46 | } 47 | }, 48 | { 49 | "vertex_shader": "minecraft:core/screenquad", 50 | "fragment_shader": "minecraft:post/blit", 51 | "inputs": [ 52 | { 53 | "sampler_name": "In", 54 | "target": "swap" 55 | } 56 | ], 57 | "uniforms": { 58 | "BlitConfig": [ 59 | { 60 | "name": "ColorModulate", 61 | "type": "vec4", 62 | "value": [ 63 | 1.0, 64 | 1.0, 65 | 1.0, 66 | 1.0 67 | ] 68 | } 69 | ] 70 | }, 71 | "output": "minecraft:main" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/desaturated.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/color_convolve", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main" 13 | } 14 | ], 15 | "output": "swap", 16 | "uniforms": { 17 | "ColorConfig": [ 18 | { 19 | "name": "RedMatrix", 20 | "type": "vec3", 21 | "value": [ 22 | 0.5, 23 | 0.25, 24 | 0.25 25 | ] 26 | }, 27 | { 28 | "name": "GreenMatrix", 29 | "type": "vec3", 30 | "value": [ 31 | 0.25, 32 | 0.5, 33 | 0.25 34 | ] 35 | }, 36 | { 37 | "name": "BlueMatrix", 38 | "type": "vec3", 39 | "value": [ 40 | 0.25, 41 | 0.25, 42 | 0.5 43 | ] 44 | } 45 | ] 46 | } 47 | }, 48 | { 49 | "vertex_shader": "minecraft:core/screenquad", 50 | "fragment_shader": "minecraft:post/blit", 51 | "inputs": [ 52 | { 53 | "sampler_name": "In", 54 | "target": "swap" 55 | } 56 | ], 57 | "uniforms": { 58 | "BlitConfig": [ 59 | { 60 | "name": "ColorModulate", 61 | "type": "vec4", 62 | "value": [ 63 | 1.0, 64 | 1.0, 65 | 1.0, 66 | 1.0 67 | ] 68 | } 69 | ] 70 | }, 71 | "output": "minecraft:main" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/oversaturated.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/color_convolve", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main" 13 | } 14 | ], 15 | "output": "swap", 16 | "uniforms": { 17 | "ColorConfig": [ 18 | { 19 | "name": "RedMatrix", 20 | "type": "vec3", 21 | "value": [ 22 | 1.25, 23 | 0.0, 24 | 0.0 25 | ] 26 | }, 27 | { 28 | "name": "GreenMatrix", 29 | "type": "vec3", 30 | "value": [ 31 | 0.0, 32 | 1.25, 33 | 0.0 34 | ] 35 | }, 36 | { 37 | "name": "BlueMatrix", 38 | "type": "vec3", 39 | "value": [ 40 | 0.0, 41 | 0.0, 42 | 1.25 43 | ] 44 | } 45 | ] 46 | } 47 | }, 48 | { 49 | "vertex_shader": "minecraft:core/screenquad", 50 | "fragment_shader": "minecraft:post/blit", 51 | "inputs": [ 52 | { 53 | "sampler_name": "In", 54 | "target": "swap" 55 | } 56 | ], 57 | "uniforms": { 58 | "BlitConfig": [ 59 | { 60 | "name": "ColorModulate", 61 | "type": "vec4", 62 | "value": [ 63 | 1.0, 64 | 1.0, 65 | 1.0, 66 | 1.0 67 | ] 68 | } 69 | ] 70 | }, 71 | "output": "minecraft:main" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/sepia.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/color_convolve", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main" 13 | } 14 | ], 15 | "output": "swap", 16 | "uniforms": { 17 | "ColorConfig": [ 18 | { 19 | "name": "RedMatrix", 20 | "type": "vec3", 21 | "value": [ 22 | 0.393, 23 | 0.769, 24 | 0.189 25 | ] 26 | }, 27 | { 28 | "name": "GreenMatrix", 29 | "type": "vec3", 30 | "value": [ 31 | 0.349, 32 | 0.686, 33 | 0.168 34 | ] 35 | }, 36 | { 37 | "name": "BlueMatrix", 38 | "type": "vec3", 39 | "value": [ 40 | 0.272, 41 | 0.534, 42 | 0.131 43 | ] 44 | } 45 | ] 46 | } 47 | }, 48 | { 49 | "vertex_shader": "minecraft:core/screenquad", 50 | "fragment_shader": "minecraft:post/blit", 51 | "inputs": [ 52 | { 53 | "sampler_name": "In", 54 | "target": "swap" 55 | } 56 | ], 57 | "uniforms": { 58 | "BlitConfig": [ 59 | { 60 | "name": "ColorModulate", 61 | "type": "vec4", 62 | "value": [ 63 | 1.0, 64 | 1.0, 65 | 1.0, 66 | 1.0 67 | ] 68 | } 69 | ] 70 | }, 71 | "output": "minecraft:main" 72 | } 73 | ] 74 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/pt_br.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Porta retrato", 3 | "item.camera.camera": "Camera", 4 | "item.camera.image": "Imagem", 5 | "item.camera.image_frame": "Porta retrato", 6 | "item.camera.album": "Álbum", 7 | "message.no_consumable": "Não há material suficiente no inventário", 8 | "message.image_cooldown": "A camera está processando", 9 | "message.upload_error": "Erro ao carregar: %s", 10 | "tooltip.image_time": "Tirada as %s", 11 | "tooltip.image_owner": "Tirada por %s", 12 | "tooltip.image_frame_empty": "Sem imagem", 13 | "button.camera.prev": "Anterior", 14 | "button.camera.next": "Próxima", 15 | "button.camera.upload": "Carregar", 16 | "gui.camera.choose_filter": "Escolha um Filtro", 17 | "gui.camera.upload_image": "Carregue uma Imagem", 18 | "gui.camera.title": "Camera", 19 | "gui.frame.resize": "Redimensione o Quadro", 20 | "gui.frame.resize_description": "Segure SHIFT para reduzir", 21 | "tooltip.visibility": "Alterar visibilidade", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Álbum", 24 | "gui.image.title": "Imagem", 25 | "shader.none": "Nenhum", 26 | "shader.black_and_white": "Preto e Branco", 27 | "shader.sepia": "Sepia", 28 | "shader.desaturated": "Desaturado", 29 | "shader.overexposed": "Subexposta", 30 | "shader.oversaturated": "Supersaturado", 31 | "shader.blurry": "Desfoque", 32 | "shader.inverted": "Invertido", 33 | "button.frame.left": "Esquerda", 34 | "button.frame.right": "Direita", 35 | "button.frame.up": "Cima", 36 | "button.frame.down": "Baixo", 37 | "key.next_image": "Próxima Imagem", 38 | "key.previous_image": "Imagem Anterior", 39 | "filetype.images": "Imagens", 40 | "title.choose_image": "Escolha a Imagem", 41 | "sound.camera.take_image": "Camera tirando uma foto" 42 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/black_and_white.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/color_convolve", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main" 13 | } 14 | ], 15 | "output": "swap", 16 | "uniforms": { 17 | "ColorConfig": [ 18 | { 19 | "name": "RedMatrix", 20 | "type": "vec3", 21 | "value": [ 22 | 0.3333, 23 | 0.3333, 24 | 0.3333 25 | ] 26 | }, 27 | { 28 | "name": "GreenMatrix", 29 | "type": "vec3", 30 | "value": [ 31 | 0.3333, 32 | 0.3333, 33 | 0.3333 34 | ] 35 | }, 36 | { 37 | "name": "BlueMatrix", 38 | "type": "vec3", 39 | "value": [ 40 | 0.3333, 41 | 0.3333, 42 | 0.3333 43 | ] 44 | } 45 | ] 46 | } 47 | }, 48 | { 49 | "vertex_shader": "minecraft:core/screenquad", 50 | "fragment_shader": "minecraft:post/blit", 51 | "inputs": [ 52 | { 53 | "sampler_name": "In", 54 | "target": "swap" 55 | } 56 | ], 57 | "uniforms": { 58 | "BlitConfig": [ 59 | { 60 | "name": "ColorModulate", 61 | "type": "vec4", 62 | "value": [ 63 | 1.0, 64 | 1.0, 65 | 1.0, 66 | 1.0 67 | ] 68 | } 69 | ] 70 | }, 71 | "output": "minecraft:main" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageImageUnavailable.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.TextureCache; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.protocol.PacketFlow; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.neoforged.neoforge.network.handling.IPayloadContext; 11 | 12 | import java.awt.image.BufferedImage; 13 | import java.util.UUID; 14 | 15 | public class MessageImageUnavailable implements Message { 16 | 17 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "image_unavailable")); 18 | 19 | private UUID imgUUID; 20 | 21 | public MessageImageUnavailable() { 22 | 23 | } 24 | 25 | public MessageImageUnavailable(UUID imgUUID) { 26 | this.imgUUID = imgUUID; 27 | } 28 | 29 | @Override 30 | public PacketFlow getExecutingSide() { 31 | return PacketFlow.CLIENTBOUND; 32 | } 33 | 34 | @Override 35 | public void executeClientSide(IPayloadContext context) { 36 | TextureCache.instance().addImage(imgUUID, new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)); 37 | } 38 | 39 | @Override 40 | public MessageImageUnavailable fromBytes(RegistryFriendlyByteBuf buf) { 41 | imgUUID = buf.readUUID(); 42 | return this; 43 | } 44 | 45 | @Override 46 | public void toBytes(RegistryFriendlyByteBuf buf) { 47 | buf.writeUUID(imgUUID); 48 | } 49 | 50 | @Override 51 | public Type type() { 52 | return TYPE; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ClientConfig.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import de.maxhenkel.corelib.config.ConfigBase; 4 | 5 | import java.text.SimpleDateFormat; 6 | 7 | import net.neoforged.fml.event.config.ModConfigEvent; 8 | import net.neoforged.neoforge.common.ModConfigSpec; 9 | 10 | public class ClientConfig extends ConfigBase { 11 | 12 | private final ModConfigSpec.ConfigValue imageDateFormatSpec; 13 | public final ModConfigSpec.ConfigValue lastImagePath; 14 | public final ModConfigSpec.BooleanValue renderImageItem; 15 | public final ModConfigSpec.DoubleValue resizeGuiOpacity; 16 | 17 | public SimpleDateFormat imageDateFormat; 18 | 19 | public ClientConfig(ModConfigSpec.Builder builder) { 20 | super(builder); 21 | imageDateFormatSpec = builder 22 | .comment("The format the date will be displayed on the image") 23 | .define("image_date_format", "MM/dd/yyyy HH:mm"); 24 | lastImagePath = builder.define("last_image_path", ""); 25 | renderImageItem = builder 26 | .comment("If the image item should render the actual image") 27 | .define("render_image_item", true); 28 | resizeGuiOpacity = builder 29 | .comment("The opacity of the resize image frame GUI") 30 | .defineInRange("resize_gui_opacity", 1D, 0D, 1D); 31 | } 32 | 33 | @Override 34 | public void onLoad(ModConfigEvent.Loading event) { 35 | super.onLoad(event); 36 | onConfigChanged(); 37 | } 38 | 39 | @Override 40 | public void onReload(ModConfigEvent.Reloading event) { 41 | super.onReload(event); 42 | onConfigChanged(); 43 | } 44 | 45 | private void onConfigChanged() { 46 | imageDateFormat = new SimpleDateFormat(imageDateFormatSpec.get()); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageAlbumPage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.gui.AlbumContainer; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.protocol.PacketFlow; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.server.level.ServerPlayer; 11 | import net.neoforged.neoforge.network.handling.IPayloadContext; 12 | 13 | public class MessageAlbumPage implements Message { 14 | 15 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "album_page")); 16 | 17 | private int page; 18 | 19 | public MessageAlbumPage() { 20 | 21 | } 22 | 23 | public MessageAlbumPage(int page) { 24 | this.page = page; 25 | } 26 | 27 | @Override 28 | public PacketFlow getExecutingSide() { 29 | return PacketFlow.SERVERBOUND; 30 | } 31 | 32 | @Override 33 | public void executeServerSide(IPayloadContext context) { 34 | if (!(context.player() instanceof ServerPlayer sender)) { 35 | return; 36 | } 37 | if (sender.containerMenu instanceof AlbumContainer container) { 38 | container.setPage(page); 39 | } 40 | } 41 | 42 | @Override 43 | public MessageAlbumPage fromBytes(RegistryFriendlyByteBuf buf) { 44 | page = buf.readInt(); 45 | return this; 46 | } 47 | 48 | @Override 49 | public void toBytes(RegistryFriendlyByteBuf buf) { 50 | buf.writeInt(page); 51 | } 52 | 53 | @Override 54 | public Type type() { 55 | return TYPE; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/no_no.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Bilderamme", 3 | "item.camera.camera": "Kamera", 4 | "item.camera.image": "Bilde", 5 | "item.camera.image_frame": "Bilderamme", 6 | "item.camera.album": "Album", 7 | "message.no_consumable": "Ingen eller ikke nok materiale i ryggsekken", 8 | "message.image_cooldown": "Kameraet kjøles ned", 9 | "message.upload_error": "Bildeopplastingsfeil: %s", 10 | "tooltip.image_time": "Tatt den %s", 11 | "tooltip.image_owner": "Tatt av %s", 12 | "tooltip.image_frame_empty": "Ingen Bilde", 13 | "button.camera.prev": "Forrige", 14 | "button.camera.next": "Neste", 15 | "button.camera.upload": "Last Opp", 16 | "gui.camera.choose_filter": "Velg ett filter", 17 | "gui.camera.upload_image": "Last opp et bilde", 18 | "gui.camera.title": "Kamera", 19 | "gui.frame.resize": "Endre rammestørrelse", 20 | "gui.frame.resize_description": "Hold SHIFT for å krympe", 21 | "tooltip.visibility": "Veksle synlighet", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Album", 24 | "gui.image.title": "Bilde", 25 | "shader.none": "Ingen", 26 | "shader.black_and_white": "Sort og Hvitt", 27 | "shader.sepia": "Sepia", 28 | "shader.desaturated": "Undermettet", 29 | "shader.overexposed": "Overeksponert", 30 | "shader.oversaturated": "Oversaturated", 31 | "shader.blurry": "Slørette", 32 | "shader.inverted": "Omvendt", 33 | "button.frame.left": "Venstre", 34 | "button.frame.right": "Høyre", 35 | "button.frame.up": "Opp", 36 | "button.frame.down": "Ned", 37 | "key.next_image": "Neste bilde", 38 | "key.previous_image": "Forrige bilde", 39 | "filetype.images": "Bilder", 40 | "title.choose_image": "Velg bilde", 41 | "sound.camera.take_image": "Kamera tar ett bilde", 42 | "jei.camera.take_image": "Ta ett bilde", 43 | "jei.camera.tooltip.take_image": "Forbruker %sx %s fra ryggsekken din når du tar ett bilde" 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/de_de.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Bilderrahmen", 3 | "item.camera.camera": "Kamera", 4 | "item.camera.image": "Bild", 5 | "item.camera.image_frame": "Bilderrahmen", 6 | "item.camera.album": "Album", 7 | "message.no_consumable": "Kein oder nicht genug material im Inventar", 8 | "message.image_cooldown": "Kamera ist im cooldown", 9 | "message.upload_error": "Bild uploadfehler: %s", 10 | "tooltip.image_time": "Am %s gemacht", 11 | "tooltip.image_owner": "Von %s", 12 | "tooltip.image_frame_empty": "Kein Bild", 13 | "button.camera.prev": "Zurück", 14 | "button.camera.next": "Weiter", 15 | "button.camera.upload": "Hochladen", 16 | "gui.camera.choose_filter": "Wähle einen Filter aus", 17 | "gui.camera.upload_image": "Lade ein Bild hoch", 18 | "gui.camera.title": "Kamera", 19 | "gui.frame.resize": "Rahmengröße anpassen", 20 | "gui.frame.resize_description": "SHIFT zum verkleinern", 21 | "tooltip.visibility": "Sichtbarkeit umschalten", 22 | "tooltip.visibility_short": "S", 23 | "gui.album.title": "Album", 24 | "gui.image.title": "Bild", 25 | "shader.none": "Keiner", 26 | "shader.black_and_white": "Schwarzweiss", 27 | "shader.sepia": "Sepia", 28 | "shader.desaturated": "Entsättigt", 29 | "shader.overexposed": "Overexposed", 30 | "shader.oversaturated": "Überbelichtet", 31 | "shader.blurry": "Unscharf", 32 | "shader.inverted": "Invertiert", 33 | "button.frame.left": "Links", 34 | "button.frame.right": "Rechts", 35 | "button.frame.up": "Hoch", 36 | "button.frame.down": "Runter", 37 | "key.next_image": "Nächstes Bild", 38 | "key.previous_image": "Vorheriges Bild", 39 | "filetype.images": "Bilder", 40 | "title.choose_image": "Bild auswählen", 41 | "sound.camera.take_image": "Eine Kamera, die ein Bild aufnimmt", 42 | "jei.camera.take_image": "Bilder aufnehmen", 43 | "jei.camera.tooltip.take_image": "Konsumiert %sx %s aus dem Inventar, wenn ein Bild aufgenommen wird" 44 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Image Frame", 3 | "item.camera.camera": "Camera", 4 | "item.camera.image": "Image", 5 | "item.camera.image_frame": "Image Frame", 6 | "item.camera.album": "Album", 7 | "message.no_consumable": "No or not enough material in inventory", 8 | "message.image_cooldown": "Camera is on cooldown", 9 | "message.upload_error": "Image upload error: %s", 10 | "tooltip.image_time": "Taken on %s", 11 | "tooltip.image_owner": "Taken by %s", 12 | "tooltip.image_frame_empty": "No image", 13 | "button.camera.prev": "Previous", 14 | "button.camera.next": "Next", 15 | "button.camera.upload": "Upload", 16 | "gui.camera.choose_filter": "Choose a Filter", 17 | "gui.camera.upload_image": "Upload an image", 18 | "gui.camera.title": "Camera", 19 | "gui.frame.resize": "Resize Frame", 20 | "gui.frame.resize_description": "Hold SHIFT to shrink", 21 | "tooltip.visibility": "Toggle visibility", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Album", 24 | "gui.image.title": "Image", 25 | "shader.none": "None", 26 | "shader.black_and_white": "Black and White", 27 | "shader.sepia": "Sepia", 28 | "shader.desaturated": "Desaturated", 29 | "shader.overexposed": "Overexposed", 30 | "shader.oversaturated": "Oversaturated", 31 | "shader.blurry": "Blurry", 32 | "shader.inverted": "Inverted", 33 | "button.frame.left": "Left", 34 | "button.frame.right": "Right", 35 | "button.frame.up": "Up", 36 | "button.frame.down": "Down", 37 | "key.next_image": "Next Image", 38 | "key.previous_image": "Previous Image", 39 | "filetype.images": "Images", 40 | "title.choose_image": "Choose Image", 41 | "sound.camera.take_image": "Camera taking an image", 42 | "jei.camera.take_image": "Taking an image", 43 | "jei.camera.tooltip.take_image": "Consumes %sx %s from your inventory when taking an image", 44 | "config.jade.plugin_camera.image_frame": "Image Frame" 45 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageDisableCameraMode.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.corelib.net.Message; 5 | import net.minecraft.network.RegistryFriendlyByteBuf; 6 | import net.minecraft.network.protocol.PacketFlow; 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 8 | import net.minecraft.resources.Identifier; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.minecraft.world.InteractionHand; 11 | import net.minecraft.world.item.ItemStack; 12 | import net.neoforged.neoforge.network.handling.IPayloadContext; 13 | 14 | public class MessageDisableCameraMode implements Message { 15 | 16 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "disable_camera_mode")); 17 | 18 | public MessageDisableCameraMode() { 19 | 20 | } 21 | 22 | @Override 23 | public PacketFlow getExecutingSide() { 24 | return PacketFlow.SERVERBOUND; 25 | } 26 | 27 | @Override 28 | public void executeServerSide(IPayloadContext context) { 29 | if (!(context.player() instanceof ServerPlayer sender)) { 30 | return; 31 | } 32 | for (InteractionHand hand : InteractionHand.values()) { 33 | ItemStack stack = sender.getItemInHand(hand); 34 | if (stack.getItem().equals(CameraMod.CAMERA.get())) { 35 | CameraMod.CAMERA.get().setActive(stack, false); 36 | } 37 | } 38 | } 39 | 40 | @Override 41 | public MessageDisableCameraMode fromBytes(RegistryFriendlyByteBuf buf) { 42 | return this; 43 | } 44 | 45 | @Override 46 | public void toBytes(RegistryFriendlyByteBuf buf) { 47 | 48 | } 49 | 50 | @Override 51 | public Type type() { 52 | return TYPE; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/fr_fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Cadre photo", 3 | "item.camera.camera": "Appareil photo", 4 | "item.camera.image": "Photo", 5 | "item.camera.image_frame": "Cadre photo", 6 | "item.camera.album": "Album", 7 | "message.no_consumable": "Pas ou pas assez de matériel dans l'inventaire", 8 | "message.image_cooldown": "L'appareil photo est en recharge", 9 | "message.upload_error": "Erreur d'envoie de la photo : %s", 10 | "tooltip.image_time": "Pris sur %s", 11 | "tooltip.image_owner": "Pris par %s", 12 | "tooltip.image_frame_empty": "pas de photo", 13 | "button.camera.prev": "Précédent", 14 | "button.camera.next": "Suivant", 15 | "button.camera.upload": "Envoyer", 16 | "gui.camera.choose_filter": "Choisir un filtre", 17 | "gui.camera.upload_image": "Envoyer une image", 18 | "gui.camera.title": "Camera", 19 | "gui.frame.resize": "Redimensionner le cadre", 20 | "gui.frame.resize_description": "Tenir SHIFT pour rétrécir", 21 | "tooltip.visibility": "Basculer la visibilité", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Album", 24 | "gui.image.title": "Photo", 25 | "shader.none": "Non", 26 | "shader.black_and_white": "Noir et blanc", 27 | "shader.sepia": "Sépia", 28 | "shader.desaturated": "Désaturé", 29 | "shader.overexposed": "Surexposé", 30 | "shader.oversaturated": "Sursaturé", 31 | "shader.blurry": "Flou", 32 | "shader.inverted": "Inversé", 33 | "button.frame.left": "Gauche", 34 | "button.frame.right": "Droite", 35 | "button.frame.up": "Haut", 36 | "button.frame.down": "Bas", 37 | "key.next_image": "Photo suivante", 38 | "key.previous_image": "Photo précédente", 39 | "filetype.images": "Photo", 40 | "title.choose_image": "Choisir une photo", 41 | "sound.camera.take_image": "L'appareil prenant une photo", 42 | "jei.camera.take_image": "Prendre une photo", 43 | "jei.camera.tooltip.take_image": "Consomme %sx %s de votre inventaire lors de la prise d'une photo" 44 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageUploadCustomImage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.ClientImageUploadManager; 4 | import de.maxhenkel.camera.ImageProcessor; 5 | import de.maxhenkel.camera.CameraMod; 6 | import de.maxhenkel.corelib.net.Message; 7 | import net.minecraft.network.RegistryFriendlyByteBuf; 8 | import net.minecraft.network.protocol.PacketFlow; 9 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 10 | import net.minecraft.resources.Identifier; 11 | import net.neoforged.neoforge.network.handling.IPayloadContext; 12 | 13 | import java.awt.image.BufferedImage; 14 | import java.util.UUID; 15 | 16 | public class MessageUploadCustomImage implements Message { 17 | 18 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "upload")); 19 | 20 | private UUID uuid; 21 | 22 | public MessageUploadCustomImage() { 23 | 24 | } 25 | 26 | public MessageUploadCustomImage(UUID uuid) { 27 | this.uuid = uuid; 28 | } 29 | 30 | @Override 31 | public PacketFlow getExecutingSide() { 32 | return PacketFlow.CLIENTBOUND; 33 | } 34 | 35 | @Override 36 | public void executeClientSide(IPayloadContext context) { 37 | BufferedImage image = ClientImageUploadManager.getAndRemoveImage(uuid); 38 | 39 | if (image == null) { 40 | return; 41 | } 42 | 43 | ImageProcessor.sendScreenshotThreaded(uuid, image); 44 | } 45 | 46 | @Override 47 | public MessageUploadCustomImage fromBytes(RegistryFriendlyByteBuf buf) { 48 | uuid = buf.readUUID(); 49 | return this; 50 | } 51 | 52 | @Override 53 | public void toBytes(RegistryFriendlyByteBuf buf) { 54 | buf.writeUUID(uuid); 55 | } 56 | 57 | @Override 58 | public Type type() { 59 | return TYPE; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/cs_cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Rámeček na obrázky", 3 | "item.camera.camera": "Kamera", 4 | "item.camera.image": "Obrázek", 5 | "item.camera.image_frame": "Rámeček na obrázky", 6 | "item.camera.album": "Album", 7 | "message.no_consumable": "Žádné nebo nedostatek materiálů v inventáři", 8 | "message.image_cooldown": "Kamera je v cooldownu", 9 | "message.upload_error": "Chyba při nahrávání obrázku: %s", 10 | "tooltip.image_time": "Pořízen %s", 11 | "tooltip.image_owner": "Pořízen hráčem %s", 12 | "tooltip.image_frame_empty": "Žádný obrázek", 13 | "button.camera.prev": "Předchozí", 14 | "button.camera.next": "Další", 15 | "button.camera.upload": "Nahrát", 16 | "gui.camera.choose_filter": "Vybrat filtr", 17 | "gui.camera.upload_image": "Nahrát obrázek", 18 | "gui.camera.title": "Kamera", 19 | "gui.frame.resize": "Změnit velikost rámečku", 20 | "gui.frame.resize_description": "Drž SHIFT pro zmenšení", 21 | "tooltip.visibility": "Přepnout viditelnost", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Album", 24 | "gui.image.title": "Obrázek", 25 | "shader.none": "Žádný", 26 | "shader.black_and_white": "Černá a Bílá", 27 | "shader.sepia": "Sepia", 28 | "shader.desaturated": "Desaturovaný", 29 | "shader.overexposed": "Přeexponovaný", 30 | "shader.oversaturated": "Přesycený", 31 | "shader.blurry": "Rozmazaný", 32 | "shader.inverted": "Invertovaný", 33 | "button.frame.left": "Doleva", 34 | "button.frame.right": "Doprava", 35 | "button.frame.up": "Nahoru", 36 | "button.frame.down": "Dolu", 37 | "key.next_image": "Další obrázek", 38 | "key.previous_image": "Předchozí obrázek", 39 | "filetype.images": "Obrázky", 40 | "title.choose_image": "Vybrat obrázek", 41 | "sound.camera.take_image": "Kamera pořizuje obrázek", 42 | "jei.camera.take_image": "Pořizování obrázku", 43 | "jei.camera.tooltip.take_image": "Při pořizování obrázku spotřebuje %sx %s z tvého inventáře" 44 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/uk_ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Рамка для фотографії", 3 | "item.camera.camera": "Камера", 4 | "item.camera.image": "Фотографія", 5 | "item.camera.image_frame": "Рамка для фотографії", 6 | "item.camera.album": "Альбом", 7 | "message.no_consumable": "Немає або недостатньо матеріалу в інвентарі", 8 | "message.image_cooldown": "Камера перезаряджається", 9 | "message.upload_error": "Помилка завантаження фотографії: %s", 10 | "tooltip.image_time": "Зроблено %s", 11 | "tooltip.image_owner": "Фото зробив(-ла) %s", 12 | "tooltip.image_frame_empty": "Немає фотографії", 13 | "button.camera.prev": "Попередня", 14 | "button.camera.next": "Наступна", 15 | "button.camera.upload": "Завантажити", 16 | "gui.camera.choose_filter": "Вибрати фільтр", 17 | "gui.camera.upload_image": "Завантажити фотографію", 18 | "gui.camera.title": "Камера", 19 | "gui.frame.resize": "Змінити розмір рамки", 20 | "gui.frame.resize_description": "Утримуйте SHIFT, щоб зменшити", 21 | "tooltip.visibility": "Переключити видимість", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Альбом", 24 | "gui.image.title": "Фотографія", 25 | "shader.none": "Немає", 26 | "shader.black_and_white": "Чорне й біле", 27 | "shader.sepia": "Сепія", 28 | "shader.desaturated": "Ненасиченість", 29 | "shader.overexposed": "Переекспонованість", 30 | "shader.oversaturated": "Перенасиченість", 31 | "shader.blurry": "Розмитість", 32 | "shader.inverted": "Негатив", 33 | "button.frame.left": "Ліворуч", 34 | "button.frame.right": "Праворуч", 35 | "button.frame.up": "Вверх", 36 | "button.frame.down": "Вниз", 37 | "key.next_image": "Наступна фотографія", 38 | "key.previous_image": "Попередня фотографія", 39 | "filetype.images": "Фотографії", 40 | "title.choose_image": "Виберіть фотографію", 41 | "sound.camera.take_image": "Камера робить фотографію", 42 | "jei.camera.take_image": "Зйомка фотографій", 43 | "jei.camera.tooltip.take_image": "Споживає %sx %s з вашого інвентаря під час зйомки" 44 | } -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/es_es.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Marco de imagen", 3 | "item.camera.camera": "Cámara", 4 | "item.camera.image": "Imagen", 5 | "item.camera.image_frame": "Marco de imagen", 6 | "item.camera.album": "Álbum", 7 | "message.no_consumable": "No hay o no es suficiente el material en el inventario", 8 | "message.image_cooldown": "La cámara está en reposo", 9 | "message.upload_error": "Error al cargar imagen: %s", 10 | "tooltip.image_time": "Tomada el %s", 11 | "tooltip.image_owner": "Tomada por %s", 12 | "tooltip.image_frame_empty": "Sin imagen", 13 | "button.camera.prev": "Anterior", 14 | "button.camera.next": "Siguiente", 15 | "button.camera.upload": "Cargar", 16 | "gui.camera.choose_filter": "Elegir un filtro", 17 | "gui.camera.upload_image": "Cargar una imagen", 18 | "gui.camera.title": "Cámara", 19 | "gui.frame.resize": "Redimensionar marco", 20 | "gui.frame.resize_description": "Mantener SHIFT para encoger", 21 | "tooltip.visibility": "Cambiar visibilidad", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Álbum", 24 | "gui.image.title": "Imagen", 25 | "shader.none": "Ninguno", 26 | "shader.black_and_white": "Blanco y negro", 27 | "shader.sepia": "Sepia", 28 | "shader.desaturated": "Desaturado", 29 | "shader.overexposed": "Sobreexpuesto", 30 | "shader.oversaturated": "Sobresaturado", 31 | "shader.blurry": "Borroso", 32 | "shader.inverted": "Invertido", 33 | "button.frame.left": "Izquierda", 34 | "button.frame.right": "Derecha", 35 | "button.frame.up": "Arriba", 36 | "button.frame.down": "Abajo", 37 | "key.next_image": "Imagen siguiente", 38 | "key.previous_image": "Imagen anterior", 39 | "filetype.images": "Imágenes", 40 | "title.choose_image": "Elegir imagen", 41 | "sound.camera.take_image": "Cámara tomando una imagen", 42 | "jei.camera.take_image": "Tomando una imagen", 43 | "jei.camera.tooltip.take_image": "Consume %sx %s de tu inventario al tomar una imagen", 44 | "config.jade.plugin_camera.image_frame": "Marco de imagen" 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/lang/ru_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.camera.image_frame": "Фоторамка", 3 | "item.camera.camera": "Камера", 4 | "item.camera.image": "Изображение", 5 | "item.camera.image_frame": "Фоторамка", 6 | "item.camera.album": "Альбом", 7 | "message.no_consumable": "Отсутствует или мало материала в инвентаре.", 8 | "message.image_cooldown": "Камера перезаряжается.", 9 | "message.upload_error": "Ошибка загрузки изображения: %s", 10 | "tooltip.image_time": "Снято в %s", 11 | "tooltip.image_owner": "Снято: %s", 12 | "tooltip.image_frame_empty": "Изображение отсутствует", 13 | "button.camera.prev": "Предыдущее", 14 | "button.camera.next": "Следующее", 15 | "button.camera.upload": "Загрузить", 16 | "gui.camera.choose_filter": "Выбрать фильтр", 17 | "gui.camera.upload_image": "Загрузить изображение", 18 | "gui.camera.title": "Камера", 19 | "gui.frame.resize": "Изменить размер рамки", 20 | "gui.frame.resize_description": "Удерживание Shift — уменьшить размер.", 21 | "tooltip.visibility": "Вкл./выкл. видимость", 22 | "tooltip.visibility_short": "V", 23 | "gui.album.title": "Альбом", 24 | "gui.image.title": "Изображение", 25 | "shader.none": "Нет", 26 | "shader.black_and_white": "Монохром", 27 | "shader.sepia": "Сепия", 28 | "shader.desaturated": "Ненасыщенность", 29 | "shader.overexposed": "Засвеченность", 30 | "shader.oversaturated": "Перенасыщенность", 31 | "shader.blurry": "Размытие", 32 | "shader.inverted": "Негатив", 33 | "button.frame.left": "Влево", 34 | "button.frame.right": "Вправо", 35 | "button.frame.up": "Вверх", 36 | "button.frame.down": "Вниз", 37 | "key.next_image": "Следующее изображение", 38 | "key.previous_image": "Предыдущее изображение", 39 | "filetype.images": "Изображения", 40 | "title.choose_image": "Выбрать изображение", 41 | "sound.camera.take_image": "Камера делает снимок", 42 | "jei.camera.take_image": "Создание снимка", 43 | "jei.camera.tooltip.take_image": "Тратит %sх %s из Вашего инвентаря при создании снимка.", 44 | "config.jade.plugin_camera.image_frame": "Фоторамка" 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageSetShader.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.corelib.net.Message; 5 | import net.minecraft.network.RegistryFriendlyByteBuf; 6 | import net.minecraft.network.protocol.PacketFlow; 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 8 | import net.minecraft.resources.Identifier; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.minecraft.world.InteractionHand; 11 | import net.minecraft.world.item.ItemStack; 12 | import net.neoforged.neoforge.network.handling.IPayloadContext; 13 | 14 | public class MessageSetShader implements Message { 15 | 16 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "set_shader")); 17 | 18 | private String shader; 19 | 20 | public MessageSetShader() { 21 | 22 | } 23 | 24 | public MessageSetShader(String shader) { 25 | this.shader = shader; 26 | } 27 | 28 | @Override 29 | public PacketFlow getExecutingSide() { 30 | return PacketFlow.SERVERBOUND; 31 | } 32 | 33 | @Override 34 | public void executeServerSide(IPayloadContext context) { 35 | if (!(context.player() instanceof ServerPlayer sender)) { 36 | return; 37 | } 38 | for (InteractionHand hand : InteractionHand.values()) { 39 | ItemStack stack = sender.getItemInHand(hand); 40 | if (stack.getItem().equals(CameraMod.CAMERA.get())) { 41 | stack.set(CameraMod.SHADER_DATA_COMPONENT, shader); 42 | } 43 | } 44 | } 45 | 46 | @Override 47 | public MessageSetShader fromBytes(RegistryFriendlyByteBuf buf) { 48 | shader = buf.readUtf(128); 49 | return this; 50 | } 51 | 52 | @Override 53 | public void toBytes(RegistryFriendlyByteBuf buf) { 54 | buf.writeUtf(shader); 55 | } 56 | 57 | @Override 58 | public Type type() { 59 | return TYPE; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/integration/waila/HUDHandlerImageFrame.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.integration.waila; 2 | 3 | import de.maxhenkel.camera.ImageData; 4 | import de.maxhenkel.camera.CameraMod; 5 | import de.maxhenkel.camera.entities.ImageEntity; 6 | import net.minecraft.ChatFormatting; 7 | import net.minecraft.network.chat.Component; 8 | import net.minecraft.resources.Identifier; 9 | import snownee.jade.api.*; 10 | import snownee.jade.api.config.IPluginConfig; 11 | 12 | import java.util.Date; 13 | 14 | public class HUDHandlerImageFrame implements IEntityComponentProvider { 15 | 16 | private static final Identifier OBJECT_NAME_TAG = Identifier.fromNamespaceAndPath("jade", "object_name"); 17 | 18 | public static final HUDHandlerImageFrame INSTANCE = new HUDHandlerImageFrame(); 19 | 20 | private static final Identifier UID = Identifier.fromNamespaceAndPath(CameraMod.MODID, "image_frame"); 21 | 22 | @Override 23 | public void appendTooltip(ITooltip iTooltip, EntityAccessor entityAccessor, IPluginConfig iPluginConfig) { 24 | if (entityAccessor.getEntity() instanceof ImageEntity image) { 25 | iTooltip.remove(OBJECT_NAME_TAG); 26 | iTooltip.add(image.getDisplayName().copy().withStyle(ChatFormatting.WHITE)); 27 | ImageData imageData = ImageData.fromStack(image.getItem()); 28 | if (imageData == null) { 29 | iTooltip.add(Component.translatable("tooltip.image_frame_empty")); 30 | return; 31 | } 32 | if (!imageData.getOwner().isEmpty()) { 33 | iTooltip.add(Component.translatable("tooltip.image_owner", ChatFormatting.DARK_GRAY + imageData.getOwner()).withStyle(ChatFormatting.GRAY)); 34 | } 35 | if (imageData.getTime() > 0L) { 36 | iTooltip.add(Component.translatable("tooltip.image_time", ChatFormatting.DARK_GRAY + CameraMod.CLIENT_CONFIG.imageDateFormat.format(new Date(imageData.getTime()))).withStyle(ChatFormatting.GRAY)); 37 | } 38 | } 39 | } 40 | 41 | @Override 42 | public Identifier getUid() { 43 | return UID; 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/items/ImageFrameItem.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.items; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.entities.ImageEntity; 5 | import net.minecraft.core.BlockPos; 6 | import net.minecraft.core.Direction; 7 | import net.minecraft.world.InteractionResult; 8 | import net.minecraft.world.entity.EntitySpawnReason; 9 | import net.minecraft.world.entity.player.Player; 10 | import net.minecraft.world.item.Item; 11 | import net.minecraft.world.item.ItemStack; 12 | import net.minecraft.world.item.context.UseOnContext; 13 | import net.minecraft.world.level.Level; 14 | 15 | public class ImageFrameItem extends Item { 16 | 17 | public ImageFrameItem(Properties properties) { 18 | super(properties); 19 | } 20 | 21 | @Override 22 | public InteractionResult useOn(UseOnContext context) { 23 | BlockPos pos = context.getClickedPos(); 24 | Direction facing = context.getClickedFace(); 25 | BlockPos offset = pos.relative(facing); 26 | Player player = context.getPlayer(); 27 | if (player != null && !canPlace(player, facing, context.getItemInHand(), offset)) { 28 | return InteractionResult.FAIL; 29 | } 30 | 31 | Level world = context.getLevel(); 32 | ImageEntity image = CameraMod.IMAGE_ENTITY_TYPE.get().create(world, EntitySpawnReason.COMMAND); 33 | if (image == null) { 34 | return InteractionResult.FAIL; 35 | } 36 | image.setFacing(facing); 37 | image.setImagePosition(offset); 38 | image.setOwner(context.getPlayer().getUUID()); 39 | if (image.isValid()) { 40 | if (!world.isClientSide()) { 41 | image.playPlaceSound(); 42 | world.addFreshEntity(image); 43 | } 44 | context.getItemInHand().shrink(1); 45 | return InteractionResult.SUCCESS; 46 | } 47 | return InteractionResult.FAIL; 48 | } 49 | 50 | protected boolean canPlace(Player player, Direction facing, ItemStack stack, BlockPos pos) { 51 | return !facing.getAxis().isVertical() && player.mayUseItemAt(pos, facing, stack); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/integration/jei/ImageCloneExtension.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.integration.jei; 2 | 3 | import de.maxhenkel.camera.ImageCloningRecipe; 4 | import de.maxhenkel.camera.ImageData; 5 | import de.maxhenkel.camera.CameraMod; 6 | import mezz.jei.api.constants.VanillaTypes; 7 | import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; 8 | import mezz.jei.api.gui.ingredient.ICraftingGridHelper; 9 | import mezz.jei.api.recipe.IFocusGroup; 10 | import mezz.jei.api.recipe.category.extensions.vanilla.crafting.ICraftingCategoryExtension; 11 | import net.minecraft.core.Holder; 12 | import net.minecraft.world.item.ItemStack; 13 | import net.minecraft.world.item.crafting.RecipeHolder; 14 | import net.minecraft.world.item.crafting.display.SlotDisplay; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | public class ImageCloneExtension implements ICraftingCategoryExtension { 21 | 22 | @Override 23 | public List getIngredients(RecipeHolder recipeHolder) { 24 | List result = new ArrayList<>(); 25 | ItemStack image = new ItemStack(CameraMod.IMAGE.get()); 26 | ImageData.dummy().addToImage(image); 27 | result.add(new SlotDisplay.ItemStackSlotDisplay(image)); 28 | 29 | //TODO Add paper 30 | //result.add(new SlotDisplay.TagSlotDisplay(recipeHolder.value().getPaper())); 31 | 32 | return result; 33 | } 34 | 35 | @Override 36 | public void setRecipe(RecipeHolder recipeHolder, IRecipeLayoutBuilder builder, ICraftingGridHelper craftingGridHelper, IFocusGroup focuses) { 37 | ItemStack image = new ItemStack(CameraMod.IMAGE.get()); 38 | ImageData.dummy().addToImage(image); 39 | 40 | List paper = recipeHolder.value().getPaper().getValues().stream().map(Holder::value).map(ItemStack::new).toList(); 41 | 42 | ItemStack out = image.copy(); 43 | 44 | craftingGridHelper.createAndSetInputs(builder, VanillaTypes.ITEM_STACK, Arrays.asList(List.of(image), paper), 0, 0); 45 | craftingGridHelper.createAndSetOutputs(builder, VanillaTypes.ITEM_STACK, List.of(out)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/items/ImageItem.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.items; 2 | 3 | import de.maxhenkel.camera.CameraClientMod; 4 | import de.maxhenkel.camera.ImageData; 5 | import de.maxhenkel.camera.CameraMod; 6 | import net.minecraft.ChatFormatting; 7 | import net.minecraft.network.chat.Component; 8 | import net.minecraft.world.InteractionHand; 9 | import net.minecraft.world.InteractionResult; 10 | import net.minecraft.world.entity.player.Player; 11 | import net.minecraft.world.item.Item; 12 | import net.minecraft.world.item.ItemStack; 13 | import net.minecraft.world.item.TooltipFlag; 14 | import net.minecraft.world.item.component.TooltipDisplay; 15 | import net.minecraft.world.level.Level; 16 | 17 | import java.util.Date; 18 | import java.util.function.Consumer; 19 | 20 | public class ImageItem extends Item { 21 | 22 | public ImageItem(Properties properties) { 23 | super(properties.stacksTo(1)); 24 | } 25 | 26 | @Override 27 | public InteractionResult use(Level worldIn, Player playerIn, InteractionHand handIn) { 28 | ItemStack stack = playerIn.getItemInHand(handIn); 29 | if (playerIn.level().isClientSide()) { 30 | CameraClientMod.openImageScreen(stack); 31 | } 32 | 33 | return InteractionResult.SUCCESS; 34 | } 35 | 36 | @Override 37 | public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay tooltipDisplay, Consumer consumer, TooltipFlag flag) { 38 | ImageData data = ImageData.fromStack(stack); 39 | if (data != null) { 40 | String name = data.getOwner(); 41 | if (!name.isEmpty()) { 42 | consumer.accept(Component.translatable("tooltip.image_owner", ChatFormatting.DARK_GRAY + name).withStyle(ChatFormatting.GRAY)); 43 | } 44 | long time = data.getTime(); 45 | if (time > 0L) { 46 | consumer.accept(Component.translatable("tooltip.image_time", ChatFormatting.DARK_GRAY + CameraMod.CLIENT_CONFIG.imageDateFormat.format(new Date(time))).withStyle(ChatFormatting.GRAY)); 47 | } 48 | } 49 | super.appendHoverText(stack, context, tooltipDisplay, consumer, flag); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageRequestImage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.ImageTools; 4 | import de.maxhenkel.camera.CameraMod; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.protocol.PacketFlow; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.server.level.ServerPlayer; 11 | import net.neoforged.neoforge.network.handling.IPayloadContext; 12 | 13 | import java.io.IOException; 14 | import java.util.UUID; 15 | 16 | public class MessageRequestImage implements Message { 17 | 18 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "request_image")); 19 | 20 | private UUID imgUUID; 21 | 22 | public MessageRequestImage() { 23 | 24 | } 25 | 26 | public MessageRequestImage(UUID imgUUID) { 27 | this.imgUUID = imgUUID; 28 | } 29 | 30 | @Override 31 | public PacketFlow getExecutingSide() { 32 | return PacketFlow.SERVERBOUND; 33 | } 34 | 35 | @Override 36 | public void executeServerSide(IPayloadContext context) { 37 | if (!(context.player() instanceof ServerPlayer sender)) { 38 | return; 39 | } 40 | try { 41 | byte[] data = ImageTools.toBytes(CameraMod.PACKET_MANAGER.getExistingImage(sender, imgUUID)); 42 | context.reply(new MessageImage(imgUUID, data)); 43 | } catch (IOException e) { 44 | //TODO Properly log an error 45 | e.printStackTrace(); 46 | context.reply(new MessageImageUnavailable(imgUUID)); 47 | } 48 | } 49 | 50 | @Override 51 | public MessageRequestImage fromBytes(RegistryFriendlyByteBuf buf) { 52 | imgUUID = buf.readUUID(); 53 | return this; 54 | } 55 | 56 | @Override 57 | public void toBytes(RegistryFriendlyByteBuf buf) { 58 | buf.writeUUID(imgUUID); 59 | } 60 | 61 | @Override 62 | public Type type() { 63 | return TYPE; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessagePartialImage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.corelib.net.Message; 5 | import net.minecraft.network.RegistryFriendlyByteBuf; 6 | import net.minecraft.network.protocol.PacketFlow; 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 8 | import net.minecraft.resources.Identifier; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.neoforged.neoforge.network.handling.IPayloadContext; 11 | 12 | import java.util.UUID; 13 | 14 | public class MessagePartialImage implements Message { 15 | 16 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "partial_image")); 17 | 18 | private UUID imgUUID; 19 | private int offset; 20 | private int length; 21 | private byte[] bytes; 22 | 23 | public MessagePartialImage() { 24 | 25 | } 26 | 27 | public MessagePartialImage(UUID imgUUID, int offset, int length, byte[] bytes) { 28 | this.imgUUID = imgUUID; 29 | this.offset = offset; 30 | this.length = length; 31 | this.bytes = bytes; 32 | } 33 | 34 | @Override 35 | public PacketFlow getExecutingSide() { 36 | return PacketFlow.SERVERBOUND; 37 | } 38 | 39 | @Override 40 | public void executeServerSide(IPayloadContext context) { 41 | if (!(context.player() instanceof ServerPlayer sender)) { 42 | return; 43 | } 44 | CameraMod.PACKET_MANAGER.addBytes(sender, imgUUID, offset, length, bytes); 45 | } 46 | 47 | @Override 48 | public MessagePartialImage fromBytes(RegistryFriendlyByteBuf buf) { 49 | imgUUID = buf.readUUID(); 50 | offset = buf.readInt(); 51 | length = buf.readInt(); 52 | bytes = buf.readByteArray(); 53 | return this; 54 | } 55 | 56 | @Override 57 | public void toBytes(RegistryFriendlyByteBuf buf) { 58 | buf.writeUUID(imgUUID); 59 | buf.writeInt(offset); 60 | buf.writeInt(length); 61 | 62 | buf.writeByteArray(bytes); 63 | } 64 | 65 | @Override 66 | public Type type() { 67 | return TYPE; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ImageProcessor.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import com.mojang.blaze3d.platform.NativeImage; 4 | import de.maxhenkel.camera.net.MessagePartialImage; 5 | import net.neoforged.neoforge.client.network.ClientPacketDistributor; 6 | 7 | import java.awt.image.BufferedImage; 8 | import java.io.IOException; 9 | import java.util.UUID; 10 | 11 | public class ImageProcessor { 12 | 13 | public static void sendScreenshot(UUID uuid, BufferedImage image) { 14 | byte[] data; 15 | try { 16 | data = ImageTools.optimizeImage(image); 17 | } catch (IOException e) { 18 | //TODO Properly log an error 19 | e.printStackTrace(); 20 | return; 21 | } 22 | 23 | int size = data.length; 24 | if (size < 30_000) { 25 | ClientPacketDistributor.sendToServer(new MessagePartialImage(uuid, 0, size, data)); 26 | } else { 27 | 28 | int bufferProgress = 0; 29 | byte[] currentBuffer = new byte[30_000]; 30 | for (int i = 0; i < size; i++) { 31 | if (bufferProgress >= currentBuffer.length) { 32 | ClientPacketDistributor.sendToServer(new MessagePartialImage(uuid, i - currentBuffer.length, data.length, currentBuffer)); 33 | bufferProgress = 0; 34 | currentBuffer = new byte[currentBuffer.length]; 35 | } 36 | currentBuffer[bufferProgress] = data[i]; 37 | bufferProgress++; 38 | } 39 | 40 | if (bufferProgress > 0) { 41 | byte[] rest = new byte[bufferProgress]; 42 | System.arraycopy(currentBuffer, 0, rest, 0, bufferProgress); 43 | ClientPacketDistributor.sendToServer(new MessagePartialImage(uuid, size - rest.length, data.length, rest)); 44 | } 45 | } 46 | } 47 | 48 | public static void sendScreenshotThreaded(UUID uuid, BufferedImage image) { 49 | new Thread(() -> sendScreenshot(uuid, image), "ProcessImageThread").start(); 50 | } 51 | 52 | public static void sendScreenshotThreaded(UUID uuid, NativeImage image) { 53 | new Thread(() -> sendScreenshot(uuid, ImageTools.fromNativeImage(image)), "ProcessImageThread").start(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageImage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.ImageTools; 4 | import de.maxhenkel.camera.CameraMod; 5 | import de.maxhenkel.camera.TextureCache; 6 | import de.maxhenkel.corelib.net.Message; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.network.RegistryFriendlyByteBuf; 9 | import net.minecraft.network.protocol.PacketFlow; 10 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 11 | import net.minecraft.resources.Identifier; 12 | import net.neoforged.neoforge.network.handling.IPayloadContext; 13 | 14 | import java.awt.image.BufferedImage; 15 | import java.io.IOException; 16 | import java.util.UUID; 17 | 18 | public class MessageImage implements Message { 19 | 20 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "image")); 21 | 22 | private UUID uuid; 23 | private byte[] image; 24 | 25 | public MessageImage() { 26 | 27 | } 28 | 29 | public MessageImage(UUID uuid, byte[] image) throws IOException { 30 | this.uuid = uuid; 31 | this.image = image; 32 | if (image.length > 1_000_000) { 33 | throw new IOException("Image too large: " + image.length + " bytes (max 1.000.000)"); 34 | } 35 | } 36 | 37 | @Override 38 | public PacketFlow getExecutingSide() { 39 | return PacketFlow.CLIENTBOUND; 40 | } 41 | 42 | @Override 43 | public void executeClientSide(IPayloadContext context) { 44 | try { 45 | BufferedImage img = ImageTools.fromBytes(image); 46 | Minecraft.getInstance().submitAsync(() -> TextureCache.instance().addImage(uuid, img)); 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | 52 | @Override 53 | public MessageImage fromBytes(RegistryFriendlyByteBuf buf) { 54 | uuid = buf.readUUID(); 55 | image = buf.readByteArray(); 56 | return this; 57 | } 58 | 59 | @Override 60 | public void toBytes(RegistryFriendlyByteBuf buf) { 61 | buf.writeUUID(uuid); 62 | 63 | buf.writeByteArray(image); 64 | } 65 | 66 | @Override 67 | public Type type() { 68 | return TYPE; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageRequestUploadCustomImage.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.items.CameraItem; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.chat.Component; 8 | import net.minecraft.network.protocol.PacketFlow; 9 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 10 | import net.minecraft.resources.Identifier; 11 | import net.minecraft.server.level.ServerPlayer; 12 | import net.neoforged.neoforge.network.handling.IPayloadContext; 13 | 14 | import java.util.UUID; 15 | 16 | public class MessageRequestUploadCustomImage implements Message { 17 | 18 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "request_upload")); 19 | 20 | private UUID uuid; 21 | 22 | public MessageRequestUploadCustomImage() { 23 | 24 | } 25 | 26 | public MessageRequestUploadCustomImage(UUID uuid) { 27 | this.uuid = uuid; 28 | } 29 | 30 | @Override 31 | public PacketFlow getExecutingSide() { 32 | return PacketFlow.SERVERBOUND; 33 | } 34 | 35 | @Override 36 | public void executeServerSide(IPayloadContext context) { 37 | if (!(context.player() instanceof ServerPlayer sender)) { 38 | return; 39 | } 40 | if (CameraMod.PACKET_MANAGER.canTakeImage(sender.getUUID())) { 41 | if (CameraItem.consumePaper(sender)) { 42 | context.reply(new MessageUploadCustomImage(uuid)); 43 | } else { 44 | sender.displayClientMessage(Component.translatable("message.no_consumable"), true); 45 | } 46 | } else { 47 | sender.displayClientMessage(Component.translatable("message.image_cooldown"), true); 48 | } 49 | } 50 | 51 | @Override 52 | public MessageRequestUploadCustomImage fromBytes(RegistryFriendlyByteBuf buf) { 53 | uuid = buf.readUUID(); 54 | return this; 55 | } 56 | 57 | @Override 58 | public void toBytes(RegistryFriendlyByteBuf buf) { 59 | buf.writeUUID(uuid); 60 | } 61 | 62 | @Override 63 | public Type type() { 64 | return TYPE; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/AlbumContainer.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import net.minecraft.world.Container; 5 | import net.minecraft.world.SimpleContainer; 6 | import net.minecraft.world.entity.player.Player; 7 | import net.minecraft.world.inventory.AbstractContainerMenu; 8 | import net.minecraft.world.inventory.ContainerData; 9 | import net.minecraft.world.inventory.SimpleContainerData; 10 | import net.minecraft.world.inventory.Slot; 11 | import net.minecraft.world.item.ItemStack; 12 | 13 | public class AlbumContainer extends AbstractContainerMenu { 14 | 15 | private final Container inventory; 16 | private final ContainerData intArray; 17 | 18 | public AlbumContainer(int id) { 19 | this(id, new SimpleContainer(1), new SimpleContainerData(1)); 20 | } 21 | 22 | public AlbumContainer(int id, Container inventory, ContainerData intArray) { 23 | super(CameraMod.ALBUM_CONTAINER.get(), id); 24 | checkContainerSize(inventory, 1); 25 | checkContainerDataCount(intArray, 1); 26 | this.inventory = inventory; 27 | this.intArray = intArray; 28 | addSlot(new Slot(inventory, 0, Integer.MIN_VALUE, Integer.MIN_VALUE) { 29 | @Override 30 | public void setChanged() { 31 | super.setChanged(); 32 | slotsChanged(inventory); 33 | } 34 | }); 35 | addDataSlots(intArray); 36 | } 37 | 38 | @Override 39 | public ItemStack quickMoveStack(Player playerIn, int index) { 40 | return ItemStack.EMPTY; 41 | } 42 | 43 | @Override 44 | public void setData(int id, int data) { 45 | super.setData(id, data); 46 | broadcastChanges(); 47 | } 48 | 49 | @Override 50 | public boolean stillValid(Player player) { 51 | return inventory.stillValid(player); 52 | } 53 | 54 | public ItemStack getAlbum() { 55 | return inventory.getItem(0); 56 | } 57 | 58 | public int getPage() { 59 | return intArray.get(0); 60 | } 61 | 62 | public void setPage(int page) { 63 | setData(0, page); 64 | } 65 | 66 | public void takeBook(Player player) { 67 | if (!player.mayBuild()) { 68 | return; 69 | } 70 | ItemStack itemstack = inventory.removeItemNoUpdate(0); 71 | inventory.setChanged(); 72 | if (!player.getInventory().add(itemstack)) { 73 | player.drop(itemstack, false); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/items/render/ImageSpecialRenderer.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.items.render; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import com.mojang.serialization.MapCodec; 5 | import de.maxhenkel.camera.ImageData; 6 | import de.maxhenkel.camera.CameraMod; 7 | import de.maxhenkel.camera.entities.ImageEntityRenderState; 8 | import de.maxhenkel.camera.entities.ImageRenderer; 9 | import net.minecraft.client.renderer.SubmitNodeCollector; 10 | import net.minecraft.client.renderer.special.SpecialModelRenderer; 11 | import net.minecraft.core.Direction; 12 | import net.minecraft.world.item.ItemDisplayContext; 13 | import net.minecraft.world.item.ItemStack; 14 | import org.joml.Vector3fc; 15 | 16 | import javax.annotation.Nullable; 17 | import java.util.function.Consumer; 18 | 19 | public class ImageSpecialRenderer implements SpecialModelRenderer { 20 | 21 | public ImageSpecialRenderer() { 22 | 23 | } 24 | 25 | @Override 26 | public void submit(@Nullable ImageEntityRenderState.ImageState imageState, ItemDisplayContext context, PoseStack stack, SubmitNodeCollector collector, int light, int overlay, boolean b, int i) { 27 | if (imageState == null) { 28 | return; 29 | } 30 | ImageRenderer.submitImage(imageState, Direction.SOUTH, 1, 1, light, stack, collector); 31 | } 32 | 33 | @Override 34 | public void getExtents(Consumer vecs) { 35 | 36 | } 37 | 38 | @Nullable 39 | @Override 40 | public ImageEntityRenderState.ImageState extractArgument(ItemStack stack) { 41 | if (CameraMod.CLIENT_CONFIG.renderImageItem.get()) { 42 | ImageData imageData = ImageData.fromStack(stack); 43 | if (imageData != null) { 44 | return ImageRenderer.extractImageState(imageData.getId()); 45 | } 46 | } 47 | return ImageRenderer.extractImageState(ImageRenderer.DEFAULT_IMAGE_UUID); 48 | } 49 | 50 | public static class Unbaked implements SpecialModelRenderer.Unbaked { 51 | 52 | public static final MapCodec MAP_CODEC = MapCodec.unit(ImageSpecialRenderer.Unbaked::new); 53 | 54 | public Unbaked() { 55 | 56 | } 57 | 58 | @Override 59 | @Nullable 60 | public SpecialModelRenderer bake(BakingContext context) { 61 | return new ImageSpecialRenderer(); 62 | } 63 | 64 | @Override 65 | public MapCodec type() { 66 | return MAP_CODEC; 67 | } 68 | 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/MessageResizeFrame.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.entities.ImageEntity; 5 | import de.maxhenkel.corelib.net.Message; 6 | import net.minecraft.network.RegistryFriendlyByteBuf; 7 | import net.minecraft.network.protocol.PacketFlow; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.server.level.ServerLevel; 11 | import net.minecraft.server.level.ServerPlayer; 12 | import net.minecraft.world.entity.Entity; 13 | import net.neoforged.neoforge.network.handling.IPayloadContext; 14 | 15 | import java.util.UUID; 16 | 17 | public class MessageResizeFrame implements Message { 18 | 19 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(CameraMod.MODID, "resize_frame")); 20 | 21 | private UUID uuid; 22 | private Direction direction; 23 | private boolean larger; 24 | 25 | public MessageResizeFrame() { 26 | 27 | } 28 | 29 | public MessageResizeFrame(UUID uuid, Direction direction, boolean larger) { 30 | this.uuid = uuid; 31 | this.direction = direction; 32 | this.larger = larger; 33 | } 34 | 35 | @Override 36 | public PacketFlow getExecutingSide() { 37 | return PacketFlow.SERVERBOUND; 38 | } 39 | 40 | @Override 41 | public void executeServerSide(IPayloadContext context) { 42 | if (!(context.player() instanceof ServerPlayer sender)) { 43 | return; 44 | } 45 | if (sender.level() instanceof ServerLevel serverLevel && sender.getAbilities().mayBuild) { 46 | Entity entity = serverLevel.getEntity(uuid); 47 | if (entity instanceof ImageEntity) { 48 | ImageEntity image = (ImageEntity) entity; 49 | image.resize(direction, larger); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public MessageResizeFrame fromBytes(RegistryFriendlyByteBuf buf) { 56 | uuid = buf.readUUID(); 57 | direction = Direction.values()[buf.readInt()]; 58 | larger = buf.readBoolean(); 59 | return this; 60 | } 61 | 62 | @Override 63 | public void toBytes(RegistryFriendlyByteBuf buf) { 64 | buf.writeUUID(uuid); 65 | buf.writeInt(direction.ordinal()); 66 | buf.writeBoolean(larger); 67 | } 68 | 69 | @Override 70 | public Type type() { 71 | return TYPE; 72 | } 73 | 74 | public enum Direction { 75 | UP, DOWN, LEFT, RIGHT; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: [triage] 4 | assignees: henkelmax 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | > [!WARNING] 10 | > This form is **only for bug reports**! 11 | > Please don't abuse this for feature requests or questions. 12 | > Forms that are not filled out properly will be closed without response! 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Bug description 17 | description: A clear and concise description of what the bug is. 18 | validations: 19 | required: true 20 | - type: input 21 | id: mc_version 22 | attributes: 23 | label: Minecraft version 24 | description: The Minecraft version you are using. 25 | placeholder: 1.20.4 26 | validations: 27 | required: true 28 | - type: input 29 | id: mod_version 30 | attributes: 31 | label: Mod version 32 | description: The version of the mod. 33 | placeholder: 1.20.4-1.2.3 34 | validations: 35 | required: true 36 | - type: input 37 | id: mod_loader_version 38 | attributes: 39 | label: Mod loader and version 40 | description: The mod loader and mod loader version you are using. 41 | placeholder: Fabric Loader 0.15.6 / NeoForge 20.4.1 / Forge 48.1.0 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: steps 46 | attributes: 47 | label: Steps to reproduce 48 | description: | 49 | Steps to reproduce the issue. 50 | Please **don't** report issues that are not reproducible. 51 | placeholder: | 52 | 1. Go to '...' 53 | 2. Click on '...' 54 | 3. Scroll down to '...' 55 | 4. See error 56 | validations: 57 | required: true 58 | - type: textarea 59 | id: expected 60 | attributes: 61 | label: Expected behavior 62 | description: A clear and concise description of what you expected to happen. 63 | validations: 64 | required: false 65 | - type: input 66 | id: logs 67 | attributes: 68 | label: Log files 69 | description: | 70 | Please provide log files of the game session in which the problem occurred. 71 | Don't paste the complete logs into the issue. 72 | You can use [https://gist.github.com/](https://gist.github.com/). 73 | placeholder: https://gist.github.com/exampleuser/example 74 | validations: 75 | required: true 76 | - type: textarea 77 | id: screenshots 78 | attributes: 79 | label: Screenshots 80 | description: Screenshots of the issue. 81 | validations: 82 | required: false 83 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ServerConfig.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import de.maxhenkel.corelib.config.ConfigBase; 4 | import net.neoforged.neoforge.common.ModConfigSpec; 5 | 6 | public class ServerConfig extends ConfigBase { 7 | 8 | public final ModConfigSpec.IntValue imageCooldown; 9 | public final ModConfigSpec.IntValue cameraConsumeItemAmount; 10 | public final ModConfigSpec.IntValue maxImageSize; 11 | public final ModConfigSpec.DoubleValue imageCompression; 12 | public final ModConfigSpec.BooleanValue allowImageUpload; 13 | public final ModConfigSpec.BooleanValue frameOnlyOwnerModify; 14 | public final ModConfigSpec.BooleanValue advancedImageData; 15 | public final ModConfigSpec.IntValue advancedDataMaxEntities; 16 | 17 | public ServerConfig(ModConfigSpec.Builder builder) { 18 | super(builder); 19 | imageCooldown = builder 20 | .comment("The time in milliseconds the camera will be on cooldown after taking an image") 21 | .defineInRange("camera.cooldown", 5000, 100, Integer.MAX_VALUE); 22 | cameraConsumeItemAmount = builder 23 | .comment("The amount of the item that is consumed when taking an image") 24 | .defineInRange("camera.consumed_item.amount", 1, 1, Short.MAX_VALUE); 25 | maxImageSize = builder 26 | .comment("The maximum size of an image in bytes when transferred to the server", "Higher values mean more delay/lag between taking an image and getting it into your inventory") 27 | .defineInRange("image.max_size", 200_000, 50_000, 1_000_000); 28 | imageCompression = builder 29 | .comment("The amount of jpeg compression applied to the image", "If the image exceeds the 'max_image_size', it will get compressed anyways") 30 | .defineInRange("image.compression", 0.5D, 0.1D, 1D); 31 | allowImageUpload = builder 32 | .comment("If it is allowed to upload custom images") 33 | .define("image.allow_upload", true); 34 | frameOnlyOwnerModify = builder 35 | .comment("If only the owner can modify or break the image frame") 36 | .define("image_frame.only_owner_modify", false); 37 | advancedImageData = builder 38 | .comment("If the image items should store additional data", "This isn't used by the mod itself", "Only enable this if you know what you are doing") 39 | .define("advanced_data.enable", false); 40 | advancedDataMaxEntities = builder 41 | .comment("The amount of entities that should be stored") 42 | .defineInRange("advanced_data.max_entities", 16, 1, 128); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/inventory/AlbumInventory.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.inventory; 2 | 3 | import de.maxhenkel.camera.items.ImageItem; 4 | import net.minecraft.core.NonNullList; 5 | import net.minecraft.core.component.DataComponents; 6 | import net.minecraft.world.Container; 7 | import net.minecraft.world.ContainerHelper; 8 | import net.minecraft.world.InteractionHand; 9 | import net.minecraft.world.entity.player.Player; 10 | import net.minecraft.world.item.ItemStack; 11 | import net.minecraft.world.item.component.ItemContainerContents; 12 | 13 | public class AlbumInventory implements Container { 14 | 15 | public static final int SIZE = 54; 16 | 17 | private NonNullList items; 18 | private ItemStack album; 19 | 20 | public AlbumInventory(ItemStack album) { 21 | assert !album.isEmpty(); 22 | this.album = album; 23 | this.items = NonNullList.withSize(SIZE, ItemStack.EMPTY); 24 | ItemContainerContents contents = album.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY); 25 | contents.copyInto(items); 26 | } 27 | 28 | @Override 29 | public int getContainerSize() { 30 | return SIZE; 31 | } 32 | 33 | @Override 34 | public ItemStack getItem(int index) { 35 | return items.get(index); 36 | } 37 | 38 | @Override 39 | public ItemStack removeItem(int index, int count) { 40 | ItemStack itemstack = ContainerHelper.removeItem(items, index, count); 41 | setChanged(); 42 | return itemstack; 43 | } 44 | 45 | @Override 46 | public ItemStack removeItemNoUpdate(int index) { 47 | return ContainerHelper.takeItem(items, index); 48 | } 49 | 50 | @Override 51 | public void setItem(int index, ItemStack stack) { 52 | items.set(index, stack); 53 | setChanged(); 54 | } 55 | 56 | @Override 57 | public int getMaxStackSize() { 58 | return 64; 59 | } 60 | 61 | @Override 62 | public void setChanged() { 63 | album.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(items)); 64 | } 65 | 66 | @Override 67 | public boolean canPlaceItem(int index, ItemStack stack) { 68 | return !(stack.getItem() instanceof ImageItem); 69 | } 70 | 71 | @Override 72 | public void clearContent() { 73 | items.clear(); 74 | setChanged(); 75 | } 76 | 77 | @Override 78 | public boolean isEmpty() { 79 | return items.isEmpty(); 80 | } 81 | 82 | @Override 83 | public boolean stillValid(Player player) { 84 | for (InteractionHand hand : InteractionHand.values()) { 85 | if (player.getItemInHand(hand).equals(album)) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ServerEvents.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import net.minecraft.util.TriState; 4 | import net.minecraft.world.InteractionHand; 5 | import net.minecraft.world.entity.Entity; 6 | import net.minecraft.world.entity.player.Player; 7 | import net.minecraft.world.item.ItemStack; 8 | import net.neoforged.bus.api.SubscribeEvent; 9 | import net.neoforged.fml.common.EventBusSubscriber; 10 | import net.neoforged.neoforge.event.entity.item.ItemTossEvent; 11 | import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent; 12 | import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; 13 | import net.neoforged.neoforge.event.tick.PlayerTickEvent; 14 | 15 | @EventBusSubscriber(modid = CameraMod.MODID) 16 | public class ServerEvents { 17 | 18 | @SubscribeEvent 19 | public static void onTick(PlayerTickEvent.Pre event) { 20 | if (event.getEntity().getMainHandItem().getItem().equals(CameraMod.CAMERA.get()) || event.getEntity().getOffhandItem().getItem().equals(CameraMod.CAMERA.get())) { 21 | return; 22 | } 23 | 24 | disableCamera(event.getEntity().getInventory().getSelectedItem()); 25 | 26 | for (int i = 0; i < event.getEntity().getInventory().getContainerSize(); i++) { 27 | ItemStack stack = event.getEntity().getInventory().getItem(i); 28 | disableCamera(stack); 29 | } 30 | } 31 | 32 | @SubscribeEvent 33 | public static void onRightClick(PlayerInteractEvent.RightClickBlock event) { 34 | Player player = event.getEntity(); 35 | for (InteractionHand hand : InteractionHand.values()) { 36 | ItemStack item = player.getItemInHand(hand); 37 | if (item.getItem().equals(CameraMod.CAMERA.get()) && CameraMod.CAMERA.get().isActive(item)) { 38 | event.setUseBlock(TriState.FALSE); 39 | event.setCanceled(true); 40 | break; 41 | } 42 | } 43 | } 44 | 45 | @SubscribeEvent 46 | public static void onHit(LivingIncomingDamageEvent event) { 47 | Entity source = event.getSource().getDirectEntity(); 48 | if (!(source instanceof Player)) { 49 | return; 50 | } 51 | Player player = (Player) source; 52 | for (InteractionHand hand : InteractionHand.values()) { 53 | ItemStack stack = player.getItemInHand(hand); 54 | if (stack.getItem().equals(CameraMod.CAMERA.get()) && CameraMod.CAMERA.get().isActive(stack)) { 55 | event.setCanceled(true); 56 | break; 57 | } 58 | } 59 | } 60 | 61 | @SubscribeEvent 62 | public static void onItemToss(ItemTossEvent event) { 63 | disableCamera(event.getEntity().getItem()); 64 | } 65 | 66 | private static void disableCamera(ItemStack stack) { 67 | if (stack.getItem().equals(CameraMod.CAMERA.get())) { 68 | CameraMod.CAMERA.get().setActive(stack, false); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/LecternAlbumScreen.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.net.MessageAlbumPage; 5 | import de.maxhenkel.camera.net.MessageTakeBook; 6 | import net.minecraft.client.gui.components.Button; 7 | import net.minecraft.network.chat.Component; 8 | import net.minecraft.world.entity.player.Inventory; 9 | import net.minecraft.world.inventory.AbstractContainerMenu; 10 | import net.minecraft.world.inventory.ContainerListener; 11 | import net.minecraft.world.item.ItemStack; 12 | import net.neoforged.neoforge.client.network.ClientPacketDistributor; 13 | 14 | public class LecternAlbumScreen extends AlbumScreen { 15 | 16 | private final AlbumContainer albumContainer; 17 | private final ContainerListener listener = new ContainerListener() { 18 | @Override 19 | public void slotChanged(AbstractContainerMenu containerToSend, int slotInd, ItemStack stack) { 20 | updateContents(); 21 | } 22 | 23 | @Override 24 | public void dataChanged(AbstractContainerMenu containerIn, int varToUpdate, int newValue) { 25 | if (varToUpdate == 0) { 26 | updatePage(); 27 | } 28 | } 29 | }; 30 | 31 | public LecternAlbumScreen(AlbumContainer albumContainer, Inventory inv, Component titleIn) { 32 | super(albumContainer, inv, titleIn); 33 | this.albumContainer = albumContainer; 34 | } 35 | 36 | @Override 37 | public AlbumContainer getMenu() { 38 | return this.albumContainer; 39 | } 40 | 41 | @Override 42 | protected void init() { 43 | super.init(); 44 | albumContainer.addSlotListener(listener); 45 | 46 | if (minecraft.player.mayBuild()) { 47 | addRenderableWidget(Button.builder(Component.translatable("lectern.take_book"), (button) -> { 48 | ClientPacketDistributor.sendToServer(new MessageTakeBook()); 49 | }).bounds(width / 2 - 50, height - 25, 100, 20).build()); 50 | } 51 | } 52 | 53 | @Override 54 | public void onClose() { 55 | super.onClose(); 56 | albumContainer.removeSlotListener(listener); 57 | } 58 | 59 | @Override 60 | protected void next() { 61 | super.next(); 62 | sendPageUpdate(index); 63 | } 64 | 65 | @Override 66 | protected void previous() { 67 | super.previous(); 68 | sendPageUpdate(index); 69 | } 70 | 71 | private void sendPageUpdate(int page) { 72 | ClientPacketDistributor.sendToServer(new MessageAlbumPage(page)); 73 | } 74 | 75 | @Override 76 | protected void playPageTurnSound() { 77 | 78 | } 79 | 80 | @Override 81 | public boolean isPauseScreen() { 82 | return false; 83 | } 84 | 85 | private void updateContents() { 86 | images = CameraMod.ALBUM.get().getImages(albumContainer.getAlbum()); 87 | } 88 | 89 | private void updatePage() { 90 | setIndex(albumContainer.getPage()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/CameraClientMod.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import de.maxhenkel.camera.entities.ImageRenderer; 4 | import de.maxhenkel.camera.gui.*; 5 | import de.maxhenkel.camera.items.render.ImageSpecialRenderer; 6 | import net.minecraft.client.KeyMapping; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.client.renderer.entity.EntityRenderers; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.world.item.ItemStack; 11 | import net.neoforged.api.distmarker.Dist; 12 | import net.neoforged.bus.api.IEventBus; 13 | import net.neoforged.bus.api.SubscribeEvent; 14 | import net.neoforged.fml.common.EventBusSubscriber; 15 | import net.neoforged.fml.common.Mod; 16 | import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; 17 | import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; 18 | import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; 19 | import net.neoforged.neoforge.client.event.RegisterSpecialModelRendererEvent; 20 | import net.neoforged.neoforge.common.NeoForge; 21 | import org.lwjgl.glfw.GLFW; 22 | 23 | import java.util.List; 24 | import java.util.UUID; 25 | 26 | @Mod(value = CameraMod.MODID, dist = Dist.CLIENT) 27 | @EventBusSubscriber(modid = CameraMod.MODID, value = Dist.CLIENT) 28 | public class CameraClientMod { 29 | 30 | public static KeyMapping KEY_NEXT; 31 | public static KeyMapping KEY_PREVIOUS; 32 | 33 | public CameraClientMod(IEventBus eventBus) { 34 | 35 | } 36 | 37 | @SubscribeEvent 38 | static void clientSetup(FMLClientSetupEvent event) { 39 | NeoForge.EVENT_BUS.register(new ClientEvents()); 40 | EntityRenderers.register(CameraMod.IMAGE_ENTITY_TYPE.get(), ImageRenderer::new); 41 | } 42 | 43 | @SubscribeEvent 44 | static void onRegisterScreens(RegisterMenuScreensEvent containers) { 45 | containers.register(CameraMod.ALBUM_INVENTORY_CONTAINER.get(), AlbumInventoryScreen::new); 46 | containers.register(CameraMod.ALBUM_CONTAINER.get(), LecternAlbumScreen::new); 47 | } 48 | 49 | @SubscribeEvent 50 | static void registerKeyBinds(RegisterKeyMappingsEvent event) { 51 | KEY_NEXT = new KeyMapping("key.next_image", GLFW.GLFW_KEY_DOWN, KeyMapping.Category.MISC); 52 | KEY_PREVIOUS = new KeyMapping("key.previous_image", GLFW.GLFW_KEY_UP, KeyMapping.Category.MISC); 53 | event.register(KEY_NEXT); 54 | event.register(KEY_PREVIOUS); 55 | } 56 | 57 | @SubscribeEvent 58 | static void registerItemModels(RegisterSpecialModelRendererEvent event) { 59 | event.register(Identifier.fromNamespaceAndPath(CameraMod.MODID, "image"), ImageSpecialRenderer.Unbaked.MAP_CODEC); 60 | } 61 | 62 | public static void openImageScreen(ItemStack stack) { 63 | Minecraft.getInstance().setScreen(new ImageScreen(stack)); 64 | } 65 | 66 | public static void openAlbumScreen(List images) { 67 | Minecraft.getInstance().setScreen(new AlbumScreen(images)); 68 | } 69 | 70 | public static void openResizeFrameScreen(UUID id) { 71 | Minecraft.getInstance().setScreen(new ResizeFrameScreen(id)); 72 | } 73 | 74 | public static void openCameraScreen(String currentShader) { 75 | Minecraft.getInstance().setScreen(new CameraScreen(currentShader)); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/TextureCache.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import com.mojang.blaze3d.platform.NativeImage; 4 | import de.maxhenkel.camera.net.MessageRequestImage; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.renderer.texture.DynamicTexture; 7 | import net.minecraft.resources.Identifier; 8 | import net.neoforged.neoforge.client.network.ClientPacketDistributor; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.UUID; 14 | import java.util.function.Supplier; 15 | 16 | public class TextureCache { 17 | 18 | private Map clientImageCache; 19 | private Map clientResourceCache; 20 | private Map awaitingImages; 21 | 22 | public static TextureCache instance; 23 | 24 | public TextureCache() { 25 | clientImageCache = new HashMap<>(); 26 | clientResourceCache = new HashMap<>(); 27 | awaitingImages = new HashMap<>(); 28 | } 29 | 30 | public void addImage(UUID uuid, BufferedImage image) { 31 | if (awaitingImages.containsKey(uuid)) { 32 | awaitingImages.remove(uuid); 33 | } 34 | 35 | Identifier resourceLocation = Identifier.fromNamespaceAndPath(CameraMod.MODID, "texures/camera/" + uuid.toString()); 36 | CameraTextureObject cameraTextureObject = new CameraTextureObject(resourceLocation::toString, ImageTools.toNativeImage(image)); 37 | clientImageCache.put(uuid, cameraTextureObject); 38 | clientResourceCache.put(uuid, resourceLocation); 39 | Minecraft.getInstance().getEntityRenderDispatcher().textureManager.register(resourceLocation, cameraTextureObject); 40 | } 41 | 42 | public Identifier getImage(UUID uuid) { 43 | CameraTextureObject cameraTextureObject = clientImageCache.get(uuid); 44 | 45 | if (checkImage(uuid, cameraTextureObject)) { 46 | return null; 47 | } 48 | return clientResourceCache.get(uuid); 49 | } 50 | 51 | private boolean checkImage(UUID uuid, CameraTextureObject cameraTextureObject) { 52 | if (cameraTextureObject == null) { 53 | if (awaitingImages.containsKey(uuid)) { 54 | if (awaitingImages.get(uuid).longValue() + 10_000 > System.currentTimeMillis()) { 55 | return true; 56 | } 57 | } 58 | awaitingImages.put(uuid, System.currentTimeMillis()); 59 | ClientPacketDistributor.sendToServer(new MessageRequestImage(uuid)); 60 | 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | public NativeImage getNativeImage(UUID uuid) { 67 | CameraTextureObject cameraTextureObject = clientImageCache.get(uuid); 68 | 69 | if (checkImage(uuid, cameraTextureObject)) { 70 | return null; 71 | } 72 | return cameraTextureObject.getPixels(); 73 | } 74 | 75 | public static class CameraTextureObject extends DynamicTexture { 76 | 77 | public CameraTextureObject(Supplier stringSupplier, NativeImage image) { 78 | super(stringSupplier, image); 79 | } 80 | } 81 | 82 | public static TextureCache instance() { 83 | if (instance == null) { 84 | instance = new TextureCache(); 85 | } 86 | return instance; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Camera Mod 4 | 5 | ## Links 6 | - [Modrinth](https://modrinth.com/mod/camera-mod) 7 | - [CurseForge](https://www.curseforge.com/minecraft/mc-mods/camera-mod) 8 | - [Credits](https://modrepo.de/minecraft/camera/credits) 9 | 10 | --- 11 | 12 | 13 | 14 | This mod adds a Camera and a frame for the images. The Camera takes real images of your game. 15 | 16 | ## Features 17 | 18 | - Images can be displayed by right-clicking the image 19 | - Images can be put into image frames 20 | - Images can be put into albums 21 | - Images can be put into lecterns 22 | - Images can be copied 23 | - Custom images can be uploaded 24 | - Image Frames can be resized up to 8 x 8 blocks 25 | - Date and photographer name are stored with the image item 26 | - The Camera consumes a piece of paper for every taken image (Configurable) 27 | - The Camera has multiple filters that can be applied 28 | - Multiplayer compatible 29 | - Images are saved in the world folder 30 | 31 | ## Taking an Image 32 | 33 | Images can be taken by right-clicking a camera. 34 | This brings you to the viewfinder of the camera. 35 | Right-clicking again takes an image of the perspective you are currently seeing. 36 | Images are saved with the same ratio as your Minecraft window. 37 | 38 | ![camera](https://i.imgur.com/BMWH1QZ.png) 39 | 40 | ![camera](https://i.imgur.com/qmsFX95.png) 41 | 42 | ![camera](https://i.imgur.com/Ykj86X6.png) 43 | 44 | ![camera](https://i.imgur.com/oBS0kDa.png) 45 | 46 | ## Filters 47 | 48 | The camera is able to apply filters to the image. By sneak + right-clicking the camera a GUI opens where the filter can be chosen. 49 | 50 | ### Available filters 51 | 52 | - Black and White 53 | - Sepia 54 | - Desaturated 55 | - Overexposed 56 | - Oversaturated 57 | - Blurry 58 | - Inverted 59 | 60 | ![camera](https://i.imgur.com/BQR1N5u.png) 61 | 62 | ![camera](https://i.imgur.com/rFe59ku.png) 63 | 64 | ![camera](https://media.giphy.com/media/U1UZT1oOstxwdRCEJz/giphy.gif) 65 | 66 | ## Zooming 67 | 68 | You can zoom in and out by scrolling. 69 | 70 | ![camera](https://media.giphy.com/media/RKSSzCEsG8gbA6hPJ4/giphy.gif) 71 | 72 | ## Viewing an Image 73 | 74 | You can either look at the image by right-clicking the image item or by putting it in an image frame. 75 | 76 | ![camera](https://i.imgur.com/tGwkhZN.png) 77 | 78 | ## The Album 79 | 80 | The album can hold up to 54 images. You can add images by sneak + right-clicking the album. You can view the contained images by right-clicking the album. By scrolling up and down or pressing the arrow-up/down key, you can switch the currently viewed image. 81 | 82 | ![camera](https://media1.giphy.com/media/hqxA6QgHthz8Pg94bT/giphy.gif) 83 | 84 | ## The Image Frame 85 | 86 | The image frame allows you to place your image in the world. Just place the image frame item on a wall. Sneak + right-clicking the image frame opens a GUI that allows you to resize the frame. Just clicking the resize buttons enlarges the frame in the given direction. If you sneak and click the button the frame will be downsize the frame in that direction. To display an image in the frame right-click it with the chosen image in hand. To remove it from the frame just right-click the frame again. 87 | 88 | ![camera](https://i.imgur.com/L9jWoMW.png) 89 | 90 | ![camera](https://i.imgur.com/T720nRH.png) 91 | 92 | ## Recipes 93 | 94 | Images can be copied by combining it with a piece of paper in the crafting table. 95 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/ImageScreen.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import com.mojang.blaze3d.platform.NativeImage; 4 | import de.maxhenkel.camera.ImageData; 5 | import de.maxhenkel.camera.CameraMod; 6 | import de.maxhenkel.camera.TextureCache; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.client.gui.GuiGraphics; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.client.renderer.RenderPipelines; 11 | import net.minecraft.network.chat.Component; 12 | import net.minecraft.resources.Identifier; 13 | import net.minecraft.world.inventory.AbstractContainerMenu; 14 | import net.minecraft.world.item.ItemStack; 15 | 16 | import javax.annotation.Nullable; 17 | import java.util.UUID; 18 | 19 | public class ImageScreen extends AbstractContainerScreen { 20 | 21 | public static final Identifier DEFAULT_IMAGE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/images/default_image.png"); 22 | 23 | @Nullable 24 | private UUID imageID; 25 | 26 | public ImageScreen(ItemStack image) { 27 | super(new DummyContainer(), Minecraft.getInstance().player.getInventory(), Component.translatable("gui.image.title")); 28 | 29 | ImageData imageData = ImageData.fromStack(image); 30 | if (imageData != null) { 31 | imageID = imageData.getId(); 32 | } 33 | } 34 | 35 | //https://stackoverflow.com/questions/6565703/math-algorithm-fit-image-to-screen-retain-aspect-ratio 36 | @Override 37 | public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { 38 | renderBlurredBackground(guiGraphics); 39 | 40 | if (imageID == null) { 41 | return; 42 | } 43 | 44 | drawImage(guiGraphics, width, height, imageID); 45 | } 46 | 47 | public static void drawImage(GuiGraphics guiGraphics, int width, int height, UUID uuid) { 48 | Identifier location = TextureCache.instance().getImage(uuid); 49 | int imageWidth; 50 | int imageHeight; 51 | 52 | 53 | if (location == null) { 54 | location = DEFAULT_IMAGE; 55 | imageWidth = 12; 56 | imageHeight = 8; 57 | } else { 58 | NativeImage image = TextureCache.instance().getNativeImage(uuid); 59 | imageWidth = image.getWidth(); 60 | imageHeight = image.getHeight(); 61 | } 62 | 63 | float scale = 0.8F; 64 | 65 | float ws = (float) width * scale; 66 | float hs = (float) height * scale; 67 | 68 | float rs = ws / hs; 69 | float ri = (float) imageWidth / (float) imageHeight; 70 | 71 | float hnew; 72 | float wnew; 73 | 74 | if (rs > ri) { 75 | wnew = (float) imageWidth * hs / (float) imageHeight; 76 | hnew = hs; 77 | } else { 78 | wnew = ws; 79 | hnew = (float) imageHeight * ws / (float) imageWidth; 80 | } 81 | 82 | float top = (hs - hnew) / 2F; 83 | float left = (ws - wnew) / 2F; 84 | 85 | left += ((1F - scale) * ws) / 2F; 86 | top += ((1F - scale) * hs) / 2F; 87 | 88 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, location, (int) left, (int) top, 0F, 0F, (int) wnew, (int) hnew, imageWidth, imageHeight, imageWidth, imageHeight); 89 | } 90 | 91 | @Override 92 | protected void renderLabels(GuiGraphics guiGraphics, int x, int y) { 93 | 94 | } 95 | 96 | @Override 97 | protected void renderBg(GuiGraphics p_283065_, float p_97788_, int p_97789_, int p_97790_) { 98 | 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/AlbumScreen.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import com.mojang.blaze3d.platform.InputConstants; 4 | import de.maxhenkel.camera.CameraClientMod; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.gui.GuiGraphics; 7 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 8 | import net.minecraft.network.chat.Component; 9 | import net.minecraft.sounds.SoundEvents; 10 | import net.minecraft.sounds.SoundSource; 11 | import net.minecraft.world.entity.player.Inventory; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | public class AlbumScreen extends AbstractContainerScreen { 18 | 19 | protected int index; 20 | protected List images; 21 | 22 | public AlbumScreen(AlbumContainer screenContainer, Inventory inv, Component titleIn, List images) { 23 | super(screenContainer, inv, titleIn); 24 | this.images = images; 25 | } 26 | 27 | public AlbumScreen(AlbumContainer screenContainer, Inventory inv, Component titleIn) { 28 | this(screenContainer, inv, titleIn, new ArrayList<>()); 29 | } 30 | 31 | public AlbumScreen(List images) { 32 | this(new AlbumContainer(-1), Minecraft.getInstance().player.getInventory(), Component.translatable("gui.album.title"), images); 33 | } 34 | 35 | @Override 36 | protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int x, int y) { 37 | 38 | } 39 | 40 | @Override 41 | public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { 42 | renderBlurredBackground(guiGraphics); 43 | 44 | if (images.isEmpty()) { 45 | return; 46 | } 47 | UUID uuid = images.get(index); 48 | ImageScreen.drawImage(guiGraphics, width, height, uuid); 49 | } 50 | 51 | @Override 52 | public boolean mouseScrolled(double mouseX, double mouseY, double deltaX, double deltaY) { 53 | if (deltaY < 0D) { 54 | next(); 55 | } else { 56 | previous(); 57 | } 58 | return true; 59 | } 60 | 61 | protected void next() { 62 | setIndex(index + 1); 63 | } 64 | 65 | protected void previous() { 66 | setIndex(index - 1); 67 | } 68 | 69 | protected void setIndex(int i) { 70 | if (index == i) { 71 | return; 72 | } 73 | index = i; 74 | if (index >= images.size()) { 75 | index = 0; 76 | } else if (index < 0) { 77 | index = images.size() - 1; 78 | } 79 | playPageTurnSound(); 80 | } 81 | 82 | protected void playPageTurnSound() { 83 | minecraft.level.playLocalSound(minecraft.player.getX(), minecraft.player.getY(), minecraft.player.getZ(), SoundEvents.BOOK_PAGE_TURN, SoundSource.MASTER, 1F, minecraft.level.random.nextFloat() * 0.1F + 0.9F, false); 84 | } 85 | 86 | private boolean wasNextDown; 87 | private boolean wasPreviousDown; 88 | 89 | @Override 90 | public void containerTick() { 91 | super.containerTick(); 92 | boolean isNextDown = InputConstants.isKeyDown(minecraft.getWindow(), CameraClientMod.KEY_NEXT.getKey().getValue()); 93 | boolean isPreviousDown = InputConstants.isKeyDown(minecraft.getWindow(), CameraClientMod.KEY_PREVIOUS.getKey().getValue()); 94 | if (wasNextDown != (wasNextDown = isNextDown) && !isNextDown) { 95 | next(); 96 | } else if (wasPreviousDown != (wasPreviousDown = isPreviousDown) && !isPreviousDown) { 97 | previous(); 98 | } 99 | } 100 | 101 | @Override 102 | protected void renderLabels(GuiGraphics guiGraphics, int x, int y) { 103 | 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/items/AlbumItem.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.items; 2 | 3 | import de.maxhenkel.camera.CameraClientMod; 4 | import de.maxhenkel.camera.ImageData; 5 | import de.maxhenkel.camera.CameraMod; 6 | import de.maxhenkel.camera.gui.AlbumInventoryContainer; 7 | import de.maxhenkel.camera.inventory.AlbumInventory; 8 | import net.minecraft.core.BlockPos; 9 | import net.minecraft.network.chat.Component; 10 | import net.minecraft.server.level.ServerPlayer; 11 | import net.minecraft.world.*; 12 | import net.minecraft.world.entity.player.Inventory; 13 | import net.minecraft.world.entity.player.Player; 14 | import net.minecraft.world.inventory.AbstractContainerMenu; 15 | import net.minecraft.world.item.Item; 16 | import net.minecraft.world.item.ItemStack; 17 | import net.minecraft.world.item.context.UseOnContext; 18 | import net.minecraft.world.level.Level; 19 | import net.minecraft.world.level.block.Blocks; 20 | import net.minecraft.world.level.block.LecternBlock; 21 | import net.minecraft.world.level.block.state.BlockState; 22 | 23 | import java.util.ArrayList; 24 | import java.util.Collections; 25 | import java.util.List; 26 | import java.util.UUID; 27 | 28 | public class AlbumItem extends Item { 29 | 30 | public AlbumItem(Properties properties) { 31 | super(properties.stacksTo(1)); 32 | } 33 | 34 | @Override 35 | public InteractionResult use(Level worldIn, Player playerIn, InteractionHand handIn) { 36 | ItemStack stack = playerIn.getItemInHand(handIn); 37 | if (playerIn.isShiftKeyDown()) { 38 | if (!playerIn.level().isClientSide() && playerIn instanceof ServerPlayer serverPlayer) { 39 | serverPlayer.openMenu(new MenuProvider() { 40 | 41 | @Override 42 | public AbstractContainerMenu createMenu(int id, Inventory playerInventory, Player playerEntity) { 43 | return new AlbumInventoryContainer(id, playerInventory, new AlbumInventory(stack)); 44 | } 45 | 46 | @Override 47 | public Component getDisplayName() { 48 | return Component.translatable(AlbumItem.this.getDescriptionId()); 49 | } 50 | }); 51 | } 52 | } else { 53 | openAlbum(playerIn, stack); 54 | } 55 | return InteractionResult.SUCCESS; 56 | } 57 | 58 | public static void openAlbum(Player player, ItemStack album) { 59 | if (player.level().isClientSide()) { 60 | List images = CameraMod.ALBUM.get().getImages(album); 61 | if (!images.isEmpty()) { 62 | CameraClientMod.openAlbumScreen(images); 63 | } 64 | } 65 | } 66 | 67 | @Override 68 | public InteractionResult useOn(UseOnContext context) { 69 | Level world = context.getLevel(); 70 | BlockPos blockpos = context.getClickedPos(); 71 | BlockState blockstate = world.getBlockState(blockpos); 72 | if (blockstate.is(Blocks.LECTERN)) { 73 | return LecternBlock.tryPlaceBook(context.getPlayer(), world, blockpos, blockstate, context.getItemInHand()) ? InteractionResult.SUCCESS : InteractionResult.PASS; 74 | } else { 75 | return InteractionResult.PASS; 76 | } 77 | } 78 | 79 | public List getImages(ItemStack stack) { 80 | if (stack.isEmpty()) { 81 | return Collections.emptyList(); 82 | } 83 | List images = new ArrayList<>(); 84 | Container inventory = new AlbumInventory(stack); 85 | for (int i = 0; i < inventory.getContainerSize(); i++) { 86 | ItemStack s = inventory.getItem(i); 87 | ImageData imageData = ImageData.fromStack(s); 88 | if (imageData == null) { 89 | continue; 90 | } 91 | images.add(imageData.getId()); 92 | } 93 | return images; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/mixins/LecternTileEntityMixin.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.mixins; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.gui.AlbumContainer; 5 | import de.maxhenkel.camera.items.AlbumItem; 6 | import net.minecraft.core.BlockPos; 7 | import net.minecraft.server.level.ServerLevel; 8 | import net.minecraft.world.Container; 9 | import net.minecraft.world.entity.player.Inventory; 10 | import net.minecraft.world.entity.player.Player; 11 | import net.minecraft.world.inventory.AbstractContainerMenu; 12 | import net.minecraft.world.inventory.ContainerData; 13 | import net.minecraft.world.item.ItemStack; 14 | import net.minecraft.world.level.block.entity.BlockEntity; 15 | import net.minecraft.world.level.block.entity.BlockEntityType; 16 | import net.minecraft.world.level.block.entity.LecternBlockEntity; 17 | import net.minecraft.world.level.block.state.BlockState; 18 | import net.minecraft.world.level.storage.ValueInput; 19 | import org.spongepowered.asm.mixin.Final; 20 | import org.spongepowered.asm.mixin.Mixin; 21 | import org.spongepowered.asm.mixin.Shadow; 22 | import org.spongepowered.asm.mixin.injection.At; 23 | import org.spongepowered.asm.mixin.injection.Inject; 24 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 25 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 26 | 27 | import javax.annotation.Nullable; 28 | 29 | @Mixin(LecternBlockEntity.class) 30 | public abstract class LecternTileEntityMixin extends BlockEntity { 31 | 32 | @Shadow 33 | ItemStack book; 34 | 35 | @Shadow 36 | int page; 37 | 38 | @Shadow 39 | private int pageCount; 40 | 41 | @Shadow 42 | @Final 43 | private Container bookAccess; 44 | 45 | @Shadow 46 | @Final 47 | private ContainerData dataAccess; 48 | 49 | public LecternTileEntityMixin(BlockEntityType type, BlockPos pos, BlockState state) { 50 | super(type, pos, state); 51 | } 52 | 53 | @Inject(method = "hasBook", at = @At("HEAD"), cancellable = true) 54 | public void hasBook(CallbackInfoReturnable cir) { 55 | cir.setReturnValue(!book.isEmpty()); 56 | } 57 | 58 | @Inject(method = "resolveBook", at = @At("HEAD"), cancellable = true) 59 | public void resolveBook(ItemStack stack, @Nullable Player player, CallbackInfoReturnable cir) { 60 | if (level instanceof ServerLevel && stack.getItem() instanceof AlbumItem) { 61 | cir.setReturnValue(stack); 62 | } 63 | } 64 | 65 | @Inject(method = "setBook(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/player/Player;)V", at = @At("HEAD"), cancellable = true) 66 | public void setBook(ItemStack stack, @Nullable Player player, CallbackInfo info) { 67 | if (!(stack.getItem() instanceof AlbumItem)) { 68 | return; 69 | } 70 | info.cancel(); 71 | book = resolveBook(stack, player); 72 | page = 0; 73 | if (level != null) { 74 | pageCount = CameraMod.ALBUM.get().getImages(book).size(); 75 | } else { 76 | pageCount = 0; 77 | } 78 | setChanged(); 79 | } 80 | 81 | @Inject(method = "createMenu", at = @At("HEAD"), cancellable = true) 82 | public void createMenu(int id, Inventory playerInventory, Player player, CallbackInfoReturnable cir) { 83 | if (!(book.getItem() instanceof AlbumItem)) { 84 | return; 85 | } 86 | cir.setReturnValue(new AlbumContainer(id, bookAccess, dataAccess)); 87 | } 88 | 89 | @Inject(method = "loadAdditional", at = @At("TAIL")) 90 | public void read(ValueInput valueInput, CallbackInfo ci) { 91 | if (!(book.getItem() instanceof AlbumItem)) { 92 | return; 93 | } 94 | pageCount = CameraMod.ALBUM.get().getImages(book).size(); 95 | } 96 | 97 | @Shadow 98 | protected abstract ItemStack resolveBook(ItemStack stack, @Nullable Player player); 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/net/PacketManager.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.net; 2 | 3 | import de.maxhenkel.camera.ImageData; 4 | import de.maxhenkel.camera.ImageTools; 5 | import de.maxhenkel.camera.CameraMod; 6 | import net.minecraft.server.level.ServerPlayer; 7 | import net.minecraft.world.Containers; 8 | import net.minecraft.world.item.ItemStack; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.io.IOException; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.UUID; 15 | 16 | public class PacketManager { 17 | 18 | private Map clientDataMap; 19 | 20 | private Map imageCache; 21 | 22 | private Map cooldowns; 23 | 24 | public PacketManager() { 25 | this.clientDataMap = new HashMap<>(); 26 | this.imageCache = new HashMap<>(); 27 | this.cooldowns = new HashMap<>(); 28 | } 29 | 30 | public void addBytes(ServerPlayer playerMP, UUID imagegID, int offset, int length, byte[] bytes) { 31 | byte[] data; 32 | if (!clientDataMap.containsKey(imagegID)) { 33 | data = new byte[length]; 34 | } else { 35 | data = clientDataMap.get(imagegID); 36 | } 37 | 38 | System.arraycopy(bytes, 0, data, offset, bytes.length); 39 | 40 | clientDataMap.put(imagegID, data); 41 | 42 | if (offset + bytes.length >= data.length) { 43 | try { 44 | BufferedImage image = completeImage(imagegID); 45 | if (image == null) { 46 | throw new IOException("Image incomplete"); 47 | } 48 | imageCache.put(imagegID, image); 49 | 50 | new Thread(() -> { 51 | try { 52 | ImageTools.saveImage(playerMP, imagegID, image); 53 | 54 | playerMP.level().getServer().submitAsync(() -> { 55 | ItemStack stack = new ItemStack(CameraMod.IMAGE.get()); 56 | ImageData imageData = ImageData.create(playerMP, imagegID); 57 | imageData.addToImage(stack); 58 | if (!playerMP.addItem(stack)) { 59 | Containers.dropItemStack(playerMP.level(), playerMP.getX(), playerMP.getY(), playerMP.getZ(), stack); 60 | } 61 | }); 62 | } catch (IOException e) { 63 | e.printStackTrace(); 64 | } 65 | }, "SaveImageThread").start(); 66 | 67 | } catch (IOException e) { 68 | e.printStackTrace(); 69 | } 70 | } 71 | } 72 | 73 | public BufferedImage getExistingImage(ServerPlayer playerMP, UUID uuid) throws IOException { 74 | if (imageCache.containsKey(uuid)) { 75 | return imageCache.get(uuid); 76 | } 77 | BufferedImage image = ImageTools.loadImage(playerMP, uuid); 78 | imageCache.put(uuid, image); 79 | return image; 80 | } 81 | 82 | public BufferedImage completeImage(UUID imgUUID) { 83 | byte[] data = clientDataMap.get(imgUUID); 84 | if (data == null) { 85 | return null; 86 | } 87 | 88 | try { 89 | BufferedImage image = ImageTools.fromBytes(data); 90 | clientDataMap.remove(imgUUID); 91 | return image; 92 | } catch (IOException e) { 93 | e.printStackTrace(); 94 | return null; 95 | } 96 | } 97 | 98 | public boolean canTakeImage(UUID player) { 99 | if (cooldowns.containsKey(player)) { 100 | if (System.currentTimeMillis() - cooldowns.get(player) < CameraMod.SERVER_CONFIG.imageCooldown.get()) { 101 | return false; 102 | } else { 103 | cooldowns.put(player, System.currentTimeMillis()); 104 | return true; 105 | } 106 | } else { 107 | cooldowns.put(player, System.currentTimeMillis()); 108 | return true; 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/resources/assets/camera/post_effect/blurry.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "swap": {} 4 | }, 5 | "passes": [ 6 | { 7 | "vertex_shader": "minecraft:core/screenquad", 8 | "fragment_shader": "minecraft:post/box_blur", 9 | "inputs": [ 10 | { 11 | "sampler_name": "In", 12 | "target": "minecraft:main", 13 | "bilinear": true 14 | } 15 | ], 16 | "output": "swap", 17 | "uniforms": { 18 | "BlurConfig": [ 19 | { 20 | "name": "BlurDir", 21 | "type": "vec2", 22 | "value": [ 23 | 1.0, 24 | 0.0 25 | ] 26 | }, 27 | { 28 | "name": "Radius", 29 | "type": "float", 30 | "value": 10.0 31 | } 32 | ] 33 | } 34 | }, 35 | { 36 | "vertex_shader": "minecraft:core/screenquad", 37 | "fragment_shader": "minecraft:post/box_blur", 38 | "inputs": [ 39 | { 40 | "sampler_name": "In", 41 | "target": "swap", 42 | "bilinear": true 43 | } 44 | ], 45 | "output": "minecraft:main", 46 | "uniforms": { 47 | "BlurConfig": [ 48 | { 49 | "name": "BlurDir", 50 | "type": "vec2", 51 | "value": [ 52 | 0.0, 53 | 1.0 54 | ] 55 | }, 56 | { 57 | "name": "Radius", 58 | "type": "float", 59 | "value": 10.0 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "vertex_shader": "minecraft:core/screenquad", 66 | "fragment_shader": "minecraft:post/box_blur", 67 | "inputs": [ 68 | { 69 | "sampler_name": "In", 70 | "target": "minecraft:main", 71 | "bilinear": true 72 | } 73 | ], 74 | "output": "swap", 75 | "uniforms": { 76 | "BlurConfig": [ 77 | { 78 | "name": "BlurDir", 79 | "type": "vec2", 80 | "value": [ 81 | 1.0, 82 | 0.0 83 | ] 84 | }, 85 | { 86 | "name": "Radius", 87 | "type": "float", 88 | "value": 5.0 89 | } 90 | ] 91 | } 92 | }, 93 | { 94 | "vertex_shader": "minecraft:core/screenquad", 95 | "fragment_shader": "minecraft:post/box_blur", 96 | "inputs": [ 97 | { 98 | "sampler_name": "In", 99 | "target": "swap", 100 | "bilinear": true 101 | } 102 | ], 103 | "output": "minecraft:main", 104 | "uniforms": { 105 | "BlurConfig": [ 106 | { 107 | "name": "BlurDir", 108 | "type": "vec2", 109 | "value": [ 110 | 0.0, 111 | 1.0 112 | ] 113 | }, 114 | { 115 | "name": "Radius", 116 | "type": "float", 117 | "value": 5.0 118 | } 119 | ] 120 | } 121 | }, 122 | { 123 | "vertex_shader": "minecraft:core/screenquad", 124 | "fragment_shader": "minecraft:post/box_blur", 125 | "inputs": [ 126 | { 127 | "sampler_name": "In", 128 | "target": "minecraft:main", 129 | "bilinear": true 130 | } 131 | ], 132 | "output": "swap", 133 | "uniforms": { 134 | "BlurConfig": [ 135 | { 136 | "name": "BlurDir", 137 | "type": "vec2", 138 | "value": [ 139 | 1.0, 140 | 0.0 141 | ] 142 | }, 143 | { 144 | "name": "Radius", 145 | "type": "float", 146 | "value": 2.5 147 | } 148 | ] 149 | } 150 | }, 151 | { 152 | "vertex_shader": "minecraft:core/screenquad", 153 | "fragment_shader": "minecraft:post/box_blur", 154 | "inputs": [ 155 | { 156 | "sampler_name": "In", 157 | "target": "swap", 158 | "bilinear": true 159 | } 160 | ], 161 | "output": "minecraft:main", 162 | "uniforms": { 163 | "BlurConfig": [ 164 | { 165 | "name": "BlurDir", 166 | "type": "vec2", 167 | "value": [ 168 | 0.0, 169 | 1.0 170 | ] 171 | }, 172 | { 173 | "name": "Radius", 174 | "type": "float", 175 | "value": 2.5 176 | } 177 | ] 178 | } 179 | } 180 | ] 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/items/CameraItem.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.items; 2 | 3 | import de.maxhenkel.camera.CameraClientMod; 4 | import de.maxhenkel.camera.CameraMod; 5 | import de.maxhenkel.camera.ModSounds; 6 | import de.maxhenkel.camera.net.MessageTakeImage; 7 | import de.maxhenkel.corelib.item.ItemUtils; 8 | import net.minecraft.network.chat.Component; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.minecraft.sounds.SoundSource; 11 | import net.minecraft.util.Unit; 12 | import net.minecraft.world.InteractionHand; 13 | import net.minecraft.world.InteractionResult; 14 | import net.minecraft.world.entity.LivingEntity; 15 | import net.minecraft.world.entity.player.Player; 16 | import net.minecraft.world.item.Item; 17 | import net.minecraft.world.item.ItemStack; 18 | import net.minecraft.world.item.ItemUseAnimation; 19 | import net.minecraft.world.level.Level; 20 | import net.neoforged.neoforge.network.PacketDistributor; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.UUID; 25 | 26 | public class CameraItem extends Item { 27 | 28 | public CameraItem(Properties properties) { 29 | super(properties.stacksTo(1)); 30 | } 31 | 32 | @Override 33 | public InteractionResult use(Level worldIn, Player playerIn, InteractionHand handIn) { 34 | ItemStack stack = playerIn.getItemInHand(handIn); 35 | 36 | if (playerIn.isShiftKeyDown() && !isActive(stack)) { 37 | if (worldIn.isClientSide()) { 38 | CameraClientMod.openCameraScreen(stack.get(CameraMod.SHADER_DATA_COMPONENT)); 39 | } 40 | return InteractionResult.SUCCESS; 41 | } 42 | 43 | if (!(playerIn instanceof ServerPlayer serverPlayer)) { 44 | return InteractionResult.SUCCESS; 45 | } 46 | 47 | if (!isActive(stack)) { 48 | CameraMod.CAMERA.get().setActive(stack, true); 49 | } else if (CameraMod.PACKET_MANAGER.canTakeImage(playerIn.getUUID())) { 50 | if (consumePaper(playerIn)) { 51 | worldIn.playSound(null, playerIn.blockPosition(), ModSounds.TAKE_IMAGE.get(), SoundSource.AMBIENT, 1F, 1F); 52 | UUID uuid = UUID.randomUUID(); 53 | PacketDistributor.sendToPlayer(serverPlayer, new MessageTakeImage(uuid)); 54 | CameraMod.CAMERA.get().setActive(stack, false); 55 | } else { 56 | playerIn.displayClientMessage(Component.translatable("message.no_consumable"), true); 57 | } 58 | } else { 59 | playerIn.displayClientMessage(Component.translatable("message.image_cooldown"), true); 60 | } 61 | return InteractionResult.SUCCESS; 62 | } 63 | 64 | @Override 65 | public int getUseDuration(ItemStack stack, LivingEntity entity) { 66 | return 50000; 67 | } 68 | 69 | @Override 70 | public ItemUseAnimation getUseAnimation(ItemStack stack) { 71 | if (isActive(stack)) { 72 | return ItemUseAnimation.BOW; 73 | } else { 74 | return ItemUseAnimation.NONE; 75 | } 76 | } 77 | 78 | public static boolean consumePaper(Player player) { 79 | if (player.getAbilities().instabuild) { 80 | return true; 81 | } 82 | 83 | int amountNeeded = CameraMod.SERVER_CONFIG.cameraConsumeItemAmount.get(); 84 | List consumeStacks = findPaper(player); 85 | 86 | int count = 0; 87 | for (ItemStack stack : consumeStacks) { 88 | count += stack.getCount(); 89 | } 90 | if (count >= amountNeeded) { 91 | for (ItemStack stack : consumeStacks) { 92 | amountNeeded -= stack.getCount() - ItemUtils.itemStackAmount(-amountNeeded, stack, null).getCount(); 93 | } 94 | return true; 95 | } 96 | 97 | return false; 98 | } 99 | 100 | private static List findPaper(Player player) { 101 | List items = new ArrayList<>(); 102 | if (isPaper(player.getItemInHand(InteractionHand.MAIN_HAND))) { 103 | items.add(player.getItemInHand(InteractionHand.MAIN_HAND)); 104 | } 105 | if (isPaper(player.getItemInHand(InteractionHand.OFF_HAND))) { 106 | items.add(player.getItemInHand(InteractionHand.OFF_HAND)); 107 | } 108 | for (int i = 0; i < player.getInventory().getContainerSize(); i++) { 109 | ItemStack itemstack = player.getInventory().getItem(i); 110 | 111 | if (isPaper(itemstack)) { 112 | items.add(itemstack); 113 | } 114 | } 115 | return items; 116 | } 117 | 118 | protected static boolean isPaper(ItemStack stack) { 119 | return stack.is(CameraMod.IMAGE_PAPER); 120 | } 121 | 122 | public boolean isActive(ItemStack stack) { 123 | return stack.has(CameraMod.ACTIVE_DATA_COMPONENT); 124 | } 125 | 126 | public void setActive(ItemStack stack, boolean active) { 127 | if (active) { 128 | stack.set(CameraMod.ACTIVE_DATA_COMPONENT, Unit.INSTANCE); 129 | } else { 130 | stack.remove(CameraMod.ACTIVE_DATA_COMPONENT); 131 | } 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ImageCloningRecipe.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import com.mojang.serialization.MapCodec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import de.maxhenkel.camera.items.ImageItem; 6 | import net.minecraft.core.HolderLookup; 7 | import net.minecraft.core.NonNullList; 8 | import net.minecraft.core.registries.BuiltInRegistries; 9 | import net.minecraft.network.RegistryFriendlyByteBuf; 10 | import net.minecraft.network.codec.StreamCodec; 11 | import net.minecraft.world.item.ItemStack; 12 | import net.minecraft.world.item.crafting.*; 13 | import net.minecraft.world.level.Level; 14 | 15 | public class ImageCloningRecipe extends CustomRecipe { 16 | 17 | private final ItemStack image; 18 | private final Ingredient paper; 19 | 20 | public ImageCloningRecipe(ItemStack image, Ingredient paper) { 21 | super(CraftingBookCategory.MISC); 22 | this.image = image; 23 | this.paper = paper; 24 | } 25 | 26 | @Override 27 | public boolean matches(CraftingInput inv, Level worldIn) { 28 | return craft(inv) != null; 29 | } 30 | 31 | @Override 32 | public NonNullList getRemainingItems(CraftingInput inv) { 33 | CraftingResult craft = craft(inv); 34 | if (craft == null) { 35 | return null; 36 | } 37 | return craft.remaining; 38 | } 39 | 40 | @Override 41 | public ItemStack assemble(CraftingInput container, HolderLookup.Provider provider) { 42 | CraftingResult craft = craft(container); 43 | if (craft == null) { 44 | return null; 45 | } 46 | return craft.result; 47 | } 48 | 49 | public ItemStack getImage() { 50 | return image; 51 | } 52 | 53 | public Ingredient getPaper() { 54 | return paper; 55 | } 56 | 57 | @Override 58 | public CraftingBookCategory category() { 59 | return CraftingBookCategory.MISC; 60 | } 61 | 62 | @Override 63 | public RecipeSerializer getSerializer() { 64 | return CameraMod.IMAGE_CLONING_SERIALIZER.get(); 65 | } 66 | 67 | public static class ImageCloningSerializer implements RecipeSerializer { 68 | 69 | private static final MapCodec CODEC = RecordCodecBuilder.mapCodec((builder) -> builder 70 | .group( 71 | BuiltInRegistries.ITEM.byNameCodec().xmap(ItemStack::new, ItemStack::getItem) 72 | .fieldOf("image") 73 | .forGetter((recipe) -> recipe.image), 74 | Ingredient.CODEC 75 | .fieldOf("paper") 76 | .forGetter((recipe) -> recipe.paper) 77 | ).apply(builder, ImageCloningRecipe::new)); 78 | 79 | private static final StreamCodec STREAM_CODEC = StreamCodec.composite( 80 | ItemStack.STREAM_CODEC, 81 | ImageCloningRecipe::getImage, 82 | Ingredient.CONTENTS_STREAM_CODEC, 83 | ImageCloningRecipe::getPaper, 84 | ImageCloningRecipe::new 85 | ); 86 | 87 | public ImageCloningSerializer() { 88 | 89 | } 90 | 91 | @Override 92 | public MapCodec codec() { 93 | return CODEC; 94 | } 95 | 96 | @Override 97 | public StreamCodec streamCodec() { 98 | return STREAM_CODEC; 99 | } 100 | } 101 | 102 | protected CraftingResult craft(RecipeInput inv) { 103 | ItemStack image = null; 104 | NonNullList remaining = NonNullList.withSize(inv.size(), ItemStack.EMPTY); 105 | int paperSlotIndex = -1; 106 | 107 | for (int i = 0; i < inv.size(); i++) { 108 | ItemStack stack = inv.getItem(i); 109 | 110 | if (stack.isEmpty()) { 111 | continue; 112 | } 113 | 114 | if (stack.getItem() instanceof ImageItem) { 115 | if (image != null) { 116 | return null; 117 | } 118 | image = stack; 119 | remaining.set(i, image.copy()); 120 | } else if (stack.is(CameraMod.IMAGE_PAPER)) { 121 | if (paperSlotIndex >= 0) { 122 | return null; 123 | } 124 | paperSlotIndex = i; 125 | } 126 | } 127 | 128 | if (image == null) { 129 | return null; 130 | } 131 | 132 | if (paperSlotIndex < 0) { 133 | return null; 134 | } 135 | 136 | ItemStack imageOut = image.copy(); 137 | imageOut.setCount(1); 138 | 139 | return new CraftingResult(imageOut, remaining); 140 | } 141 | 142 | private static class CraftingResult { 143 | public final ItemStack result; 144 | public final NonNullList remaining; 145 | 146 | public CraftingResult(ItemStack result, NonNullList remaining) { 147 | this.result = result; 148 | this.remaining = remaining; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/CameraScreen.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.ClientImageUploadManager; 4 | import de.maxhenkel.camera.ImageTools; 5 | import de.maxhenkel.camera.CameraMod; 6 | import de.maxhenkel.camera.Shaders; 7 | import de.maxhenkel.camera.net.MessageRequestUploadCustomImage; 8 | import de.maxhenkel.camera.net.MessageSetShader; 9 | import de.maxhenkel.corelib.FontColorUtils; 10 | import de.maxhenkel.corelib.inventory.ScreenBase; 11 | import net.minecraft.client.Minecraft; 12 | import net.minecraft.client.gui.GuiGraphics; 13 | import net.minecraft.client.gui.components.Button; 14 | import net.minecraft.network.chat.Component; 15 | import net.minecraft.network.chat.MutableComponent; 16 | import net.minecraft.resources.Identifier; 17 | import net.minecraft.world.inventory.AbstractContainerMenu; 18 | import net.neoforged.neoforge.client.network.ClientPacketDistributor; 19 | 20 | import javax.annotation.Nullable; 21 | import java.awt.image.BufferedImage; 22 | import java.io.IOException; 23 | import java.util.UUID; 24 | 25 | public class CameraScreen extends ScreenBase { 26 | 27 | private static final Identifier CAMERA_TEXTURE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/gui/camera.png"); 28 | private static final int PADDING = 10; 29 | private static final int BUTTON_WIDTH = 70; 30 | private static final int BUTTON_HEIGHT = 20; 31 | 32 | private int index = 0; 33 | 34 | @Nullable 35 | private Button upload; 36 | 37 | public CameraScreen(String currentShader) { 38 | super(CAMERA_TEXTURE, new DummyContainer(), Minecraft.getInstance().player.getInventory(), Component.translatable("gui.camera.title")); 39 | imageWidth = 248; 40 | imageHeight = 109; 41 | 42 | for (int i = 0; i < Shaders.SHADER_LIST.size(); i++) { 43 | String s = Shaders.SHADER_LIST.get(i); 44 | if (currentShader == null) { 45 | if (s.equals("none")) { 46 | index = i; 47 | break; 48 | } 49 | } else if (s.equals(currentShader)) { 50 | index = i; 51 | break; 52 | } 53 | } 54 | } 55 | 56 | // https://github.com/MinecraftForge/MinecraftForge/commit/007cd42ec6eed0e023c1324525cd44484ee0e79c 57 | @Override 58 | protected void init() { 59 | super.init(); 60 | clearWidgets(); 61 | addRenderableWidget(Button.builder(Component.translatable("button.camera.prev"), button -> { 62 | index--; 63 | if (index < 0) { 64 | index = Shaders.SHADER_LIST.size() - 1; 65 | } 66 | sendShader(); 67 | }).bounds(leftPos + PADDING, topPos + PADDING + font.lineHeight + PADDING, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 68 | addRenderableWidget(Button.builder(Component.translatable("button.camera.next"), button -> { 69 | index++; 70 | if (index >= Shaders.SHADER_LIST.size()) { 71 | index = 0; 72 | } 73 | sendShader(); 74 | }).bounds(leftPos + imageWidth - BUTTON_WIDTH - PADDING, topPos + PADDING + font.lineHeight + PADDING, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 75 | 76 | if (CameraMod.SERVER_CONFIG.allowImageUpload.get()) { 77 | upload = addRenderableWidget(Button.builder(Component.translatable("button.camera.upload"), button -> { 78 | ImageTools.chooseImage(file -> { 79 | try { 80 | UUID uuid = UUID.randomUUID(); 81 | BufferedImage image = ImageTools.loadImage(file); 82 | ClientImageUploadManager.addImage(uuid, image); 83 | ClientPacketDistributor.sendToServer(new MessageRequestUploadCustomImage(uuid)); 84 | } catch (IOException e) { 85 | minecraft.player.displayClientMessage(Component.translatable("message.upload_error", e.getMessage()), true); 86 | //TODO Properly log an error 87 | e.printStackTrace(); 88 | } 89 | minecraft.screen = null; 90 | }); 91 | }).bounds(leftPos + imageWidth / 2 - BUTTON_WIDTH / 2, topPos + imageHeight - BUTTON_HEIGHT - PADDING, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 92 | } 93 | } 94 | 95 | @Override 96 | public void containerTick() { 97 | super.containerTick(); 98 | if (upload != null) { 99 | upload.active = !ImageTools.isFileChooserOpen(); 100 | } 101 | } 102 | 103 | private void sendShader() { 104 | ClientPacketDistributor.sendToServer(new MessageSetShader(Shaders.SHADER_LIST.get(index))); 105 | } 106 | 107 | @Override 108 | protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { 109 | super.renderLabels(guiGraphics, mouseX, mouseY); 110 | 111 | MutableComponent chooseFilter = Component.translatable("gui.camera.choose_filter"); 112 | int chooseFilterWidth = font.width(chooseFilter); 113 | guiGraphics.drawString(font, chooseFilter.getVisualOrderText(), imageWidth / 2 - chooseFilterWidth / 2, 10, FONT_COLOR, false); 114 | 115 | MutableComponent shaderName = Component.translatable("shader." + Shaders.SHADER_LIST.get(index)); 116 | int shaderWidth = font.width(shaderName); 117 | guiGraphics.drawString(font, shaderName.getVisualOrderText(), imageWidth / 2 - shaderWidth / 2, PADDING + font.lineHeight + PADDING + BUTTON_HEIGHT / 2 - font.lineHeight / 2, FontColorUtils.WHITE, false); 118 | 119 | MutableComponent uploadImage = Component.translatable("gui.camera.upload_image"); 120 | int uploadImageWidth = font.width(uploadImage); 121 | guiGraphics.drawString(font, uploadImage.getVisualOrderText(), imageWidth / 2 - uploadImageWidth / 2, imageHeight - PADDING - BUTTON_HEIGHT - PADDING - font.lineHeight, FONT_COLOR, false); 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/gui/ResizeFrameScreen.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.gui; 2 | 3 | import de.maxhenkel.camera.CameraMod; 4 | import de.maxhenkel.camera.entities.ImageEntity; 5 | import de.maxhenkel.camera.net.MessageResizeFrame; 6 | import de.maxhenkel.corelib.FontColorUtils; 7 | import net.minecraft.ChatFormatting; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.gui.GuiGraphics; 10 | import net.minecraft.client.gui.components.Button; 11 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 12 | import net.minecraft.client.renderer.RenderPipelines; 13 | import net.minecraft.network.chat.Component; 14 | import net.minecraft.network.chat.MutableComponent; 15 | import net.minecraft.resources.Identifier; 16 | import net.minecraft.util.ARGB; 17 | import net.minecraft.world.inventory.AbstractContainerMenu; 18 | import net.minecraft.world.phys.AABB; 19 | import net.neoforged.neoforge.client.network.ClientPacketDistributor; 20 | 21 | import java.util.List; 22 | import java.util.UUID; 23 | 24 | public class ResizeFrameScreen extends AbstractContainerScreen { 25 | 26 | private static final Identifier CAMERA_TEXTURE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/gui/resize_frame.png"); 27 | private static final int PADDING = 10; 28 | private static final int BUTTON_HEIGHT = 20; 29 | private static final int BUTTON_WIDTH = 50; 30 | 31 | private UUID uuid; 32 | private float visibility; 33 | private Button visibilityButton; 34 | 35 | public ResizeFrameScreen(UUID uuid) { 36 | super(new DummyContainer(), Minecraft.getInstance().player.getInventory(), Component.translatable("gui.frame.resize")); 37 | this.uuid = uuid; 38 | visibility = CameraMod.CLIENT_CONFIG.resizeGuiOpacity.get().floatValue(); 39 | imageWidth = 248; 40 | imageHeight = 109; 41 | } 42 | 43 | @Override 44 | protected void init() { 45 | super.init(); 46 | clearWidgets(); 47 | int left = (width - imageWidth) / 2; 48 | addRenderableWidget(Button.builder(Component.empty(), (button) -> { 49 | sendMoveImage(MessageResizeFrame.Direction.LEFT); 50 | }).bounds(left + PADDING, height / 2 - BUTTON_HEIGHT / 2, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 51 | 52 | addRenderableWidget(Button.builder(Component.empty(), (button) -> { 53 | sendMoveImage(MessageResizeFrame.Direction.RIGHT); 54 | }).bounds(left + imageWidth - BUTTON_WIDTH - PADDING, height / 2 - BUTTON_HEIGHT / 2, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 55 | 56 | addRenderableWidget(Button.builder(Component.empty(), (button) -> { 57 | sendMoveImage(MessageResizeFrame.Direction.UP); 58 | }).bounds(width / 2 - BUTTON_WIDTH / 2, topPos + PADDING, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 59 | 60 | addRenderableWidget(Button.builder(Component.empty(), (button) -> { 61 | sendMoveImage(MessageResizeFrame.Direction.DOWN); 62 | }).bounds(width / 2 - BUTTON_WIDTH / 2, topPos + imageHeight - PADDING - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT).build()); 63 | 64 | visibilityButton = addRenderableWidget(Button.builder(Component.translatable("tooltip.visibility_short"), (button) -> { 65 | visibility -= 0.25; 66 | if (visibility < 0F) { 67 | visibility = 1F; 68 | } 69 | CameraMod.CLIENT_CONFIG.resizeGuiOpacity.set((double) visibility); 70 | CameraMod.CLIENT_CONFIG.resizeGuiOpacity.save(); 71 | }).bounds(left + imageWidth - 20 - PADDING, topPos + PADDING, 20, 20).build()); 72 | } 73 | 74 | private void sendMoveImage(MessageResizeFrame.Direction direction) { 75 | ClientPacketDistributor.sendToServer(new MessageResizeFrame(uuid, direction, !minecraft.hasShiftDown())); 76 | } 77 | 78 | @Override 79 | public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { 80 | if (visibility >= 1F) { 81 | renderTransparentBackground(guiGraphics); 82 | } 83 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, leftPos, topPos, 0F, 0F, imageWidth, imageHeight, imageWidth, imageHeight, 256, 256, ARGB.colorFromFloat(visibility, 1F, 1F, 1F)); 84 | 85 | super.render(guiGraphics, mouseX, mouseY, partialTicks); 86 | 87 | guiGraphics.pose().pushMatrix(); 88 | guiGraphics.pose().translate(leftPos, topPos); 89 | 90 | MutableComponent title = Component.translatable("gui.frame.resize"); 91 | int titleWidth = font.width(title); 92 | guiGraphics.drawString(font, title.getVisualOrderText(), imageWidth / 2 - titleWidth / 2, imageHeight / 2 - font.lineHeight - 1, FontColorUtils.getFontColor(ChatFormatting.DARK_GRAY), false); 93 | 94 | MutableComponent description = Component.translatable("gui.frame.resize_description"); 95 | int descriptionWidth = font.width(description); 96 | guiGraphics.drawString(font, description.getVisualOrderText(), imageWidth / 2 - descriptionWidth / 2, imageHeight / 2 + 1, FontColorUtils.getFontColor(ChatFormatting.GRAY), false); 97 | 98 | if (minecraft.hasShiftDown()) { 99 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, imageWidth / 2 - 8, PADDING + 2, 16, 109, 16, 16, 256, 256); 100 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, imageWidth / 2 - 8, imageHeight - PADDING - BUTTON_HEIGHT + 2, 0, 109, 16, 16, 256, 256); 101 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, PADDING + BUTTON_WIDTH / 2 - 8, imageHeight / 2 - BUTTON_HEIGHT / 2 + 3, 0, 125, 16, 16, 256, 256); 102 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, imageWidth - PADDING - BUTTON_WIDTH / 2 - 8, imageHeight / 2 - BUTTON_HEIGHT / 2 + 3, 16, 125, 16, 16, 256, 256); 103 | } else { 104 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, imageWidth / 2 - 8, PADDING + 2, 0, 109, 16, 16, 256, 256); 105 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, imageWidth / 2 - 8, imageHeight - PADDING - BUTTON_HEIGHT + 2, 16, 109, 16, 16, 256, 256); 106 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, PADDING + BUTTON_WIDTH / 2 - 8, imageHeight / 2 - BUTTON_HEIGHT / 2 + 3, 16, 125, 16, 16, 256, 256); 107 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, CAMERA_TEXTURE, imageWidth - PADDING - BUTTON_WIDTH / 2 - 8, imageHeight / 2 - BUTTON_HEIGHT / 2 + 3, 0, 125, 16, 16, 256, 256); 108 | } 109 | 110 | if (visibilityButton.isHovered()) { 111 | guiGraphics.setTooltipForNextFrame(font, List.of(Component.translatable("tooltip.visibility").getVisualOrderText()), mouseX, mouseY); 112 | } 113 | 114 | guiGraphics.pose().popMatrix(); 115 | } 116 | 117 | @Override 118 | protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int x, int y) { 119 | 120 | } 121 | 122 | @Override 123 | public void renderBackground(GuiGraphics guiGraphics, int x, int y, float partialTicks) { 124 | 125 | } 126 | 127 | @Override 128 | protected void renderLabels(GuiGraphics guiGraphics, int x, int y) { 129 | 130 | } 131 | 132 | private long lastCheck; 133 | 134 | @Override 135 | public void containerTick() { 136 | super.containerTick(); 137 | 138 | if (System.currentTimeMillis() - lastCheck > 500L) { 139 | if (!isImagePresent()) { 140 | minecraft.player.closeContainer(); 141 | } 142 | lastCheck = System.currentTimeMillis(); 143 | } 144 | } 145 | 146 | public boolean isImagePresent() { 147 | AABB aabb = minecraft.player.getBoundingBox(); 148 | aabb = aabb.inflate(32D); 149 | return minecraft.level.getEntitiesOfClass(ImageEntity.class, aabb).stream().anyMatch(image -> image.getUUID().equals(uuid) && image.distanceTo(minecraft.player) <= 32F); 150 | } 151 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/ClientEvents.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import de.maxhenkel.camera.items.CameraItem; 4 | import de.maxhenkel.camera.net.MessageDisableCameraMode; 5 | import net.minecraft.client.CameraType; 6 | import net.minecraft.client.Minecraft; 7 | import net.minecraft.client.gui.GuiGraphics; 8 | import net.minecraft.client.gui.screens.PauseScreen; 9 | import net.minecraft.client.multiplayer.ClientLevel; 10 | import net.minecraft.client.player.AbstractClientPlayer; 11 | import net.minecraft.client.renderer.RenderPipelines; 12 | import net.minecraft.resources.Identifier; 13 | import net.minecraft.world.InteractionHand; 14 | import net.minecraft.world.entity.player.Player; 15 | import net.minecraft.world.item.ItemStack; 16 | import net.neoforged.bus.api.SubscribeEvent; 17 | import net.neoforged.neoforge.client.event.*; 18 | import net.neoforged.neoforge.client.network.ClientPacketDistributor; 19 | 20 | import java.util.List; 21 | 22 | public class ClientEvents { 23 | 24 | private static final Identifier VIEWFINDER = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/gui/viewfinder_overlay.png"); 25 | private static final Identifier ZOOM = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/gui/zoom.png"); 26 | 27 | public static final float MAX_FOV = 90F; 28 | public static final float MIN_FOV = 5F; 29 | 30 | private Minecraft mc; 31 | private boolean inCameraMode; 32 | private float fov; 33 | private Identifier currentShader; 34 | 35 | public ClientEvents() { 36 | mc = Minecraft.getInstance(); 37 | inCameraMode = false; 38 | fov = 0F; 39 | } 40 | 41 | @SubscribeEvent 42 | public void renderOverlay(RenderGuiLayerEvent.Pre event) { 43 | inCameraMode = isInCameraMode(); 44 | 45 | if (!inCameraMode) { 46 | setShader(null); 47 | return; 48 | } 49 | 50 | event.setCanceled(true); 51 | 52 | mc.options.setCameraType(CameraType.FIRST_PERSON); 53 | 54 | setShader(getShader(mc.player)); 55 | drawViewFinder(event.getGuiGraphics()); 56 | drawZoom(event.getGuiGraphics(), getFOVPercentage()); 57 | } 58 | 59 | private void drawViewFinder(GuiGraphics guiGraphics) { 60 | int imageWidth = 192; 61 | int imageHeight = 100; 62 | 63 | float ws = (float) mc.getWindow().getGuiScaledWidth(); 64 | float hs = (float) mc.getWindow().getGuiScaledHeight(); 65 | 66 | float rs = ws / hs; 67 | float ri = (float) imageWidth / (float) imageHeight; 68 | 69 | float hnew; 70 | float wnew; 71 | 72 | if (rs > ri) { 73 | wnew = (float) imageWidth * hs / (float) imageHeight; 74 | hnew = hs; 75 | } else { 76 | wnew = ws; 77 | hnew = (float) imageHeight * ws / (float) imageWidth; 78 | } 79 | 80 | float top = (hs - hnew) / 2F; 81 | float left = (ws - wnew) / 2F; 82 | 83 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, VIEWFINDER, (int) left, (int) top, 0F, 0F, (int) wnew, (int) hnew, 192, 100, 256, 256); 84 | } 85 | 86 | private void drawZoom(GuiGraphics guiGraphics, float percent) { 87 | int zoomWidth = 112; 88 | int zoomHeight = 10; 89 | 90 | int width = mc.getWindow().getGuiScaledWidth(); 91 | int height = mc.getWindow().getGuiScaledHeight(); 92 | 93 | int left = (width - zoomWidth) / 2; 94 | int top = height / 40; 95 | 96 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ZOOM, left, top, 0F, 0F, zoomWidth, zoomHeight, zoomWidth, zoomHeight, 128, 128); 97 | int percWidth = (int) (Math.max(Math.min(percent, 1D), 0F) * (float) zoomWidth); 98 | guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ZOOM, left, top, 0F, zoomHeight, percWidth, zoomHeight, percWidth, zoomHeight, 128, 128); 99 | } 100 | 101 | 102 | @SubscribeEvent 103 | public void renderHand(RenderHandEvent event) { 104 | if (inCameraMode) { 105 | event.setCanceled(true); 106 | } 107 | } 108 | 109 | @SubscribeEvent 110 | public void onGuiOpen(ScreenEvent.Opening event) { 111 | if (inCameraMode) { 112 | if (event.getScreen() instanceof PauseScreen) { 113 | ClientPacketDistributor.sendToServer(new MessageDisableCameraMode()); 114 | event.setCanceled(true); 115 | } 116 | } 117 | } 118 | 119 | private Identifier getShader(Player player) { 120 | for (InteractionHand hand : InteractionHand.values()) { 121 | ItemStack stack = player.getItemInHand(hand); 122 | if (!stack.getItem().equals(CameraMod.CAMERA.get())) { 123 | continue; 124 | } 125 | return Shaders.getShader(stack.get(CameraMod.SHADER_DATA_COMPONENT)); 126 | } 127 | return null; 128 | } 129 | 130 | private void setShader(Identifier shader) { 131 | if (shader == null) { 132 | if (currentShader != null) { 133 | mc.gameRenderer.clearPostEffect(); 134 | } 135 | } else if (!shader.equals(currentShader)) { 136 | try { 137 | mc.gameRenderer.setPostEffect(shader); 138 | } catch (Exception e) { 139 | } 140 | } 141 | currentShader = shader; 142 | } 143 | 144 | @SubscribeEvent 145 | public void renderPlayer(ClientTickEvent.Pre event) { 146 | ClientLevel level = Minecraft.getInstance().level; 147 | if (level == null) { 148 | return; 149 | } 150 | List players = level.players(); 151 | for (AbstractClientPlayer player : players) { 152 | if (player == mc.player) { 153 | continue; 154 | } 155 | for (InteractionHand hand : InteractionHand.values()) { 156 | ItemStack stack = player.getItemInHand(hand); 157 | if (stack.getItem() instanceof CameraItem) { 158 | if (CameraMod.CAMERA.get().isActive(stack)) { 159 | player.startUsingItem(hand); 160 | } else { 161 | player.stopUsingItem(); 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | @SubscribeEvent 169 | public void onMouseEvent(InputEvent.MouseScrollingEvent event) { 170 | if (event.getScrollDeltaY() == 0D) { 171 | return; 172 | } 173 | if (!inCameraMode) { 174 | return; 175 | } 176 | 177 | if (event.getScrollDeltaY() < 0D) { 178 | fov = Math.min(fov + 5F, MAX_FOV); 179 | } else { 180 | fov = Math.max(fov - 5F, MIN_FOV); 181 | } 182 | event.setCanceled(true); 183 | } 184 | 185 | @SubscribeEvent 186 | public void onFOVModifierEvent(ViewportEvent.ComputeFov event) { 187 | if (!inCameraMode) { 188 | fov = event.getFOV(); 189 | return; 190 | } 191 | 192 | /* 193 | To trigger the rendering of the chunks that were outside of the FOV 194 | */ 195 | mc.player.setPos(mc.player.getX(), mc.player.getY() + 0.000000001D, mc.player.getZ()); 196 | 197 | event.setFOV(fov); 198 | } 199 | 200 | public float getFOVPercentage() { 201 | return 1F - (fov - MIN_FOV) / (MAX_FOV - MIN_FOV); 202 | } 203 | 204 | private ItemStack getActiveCamera() { 205 | if (mc.player == null) { 206 | return null; 207 | } 208 | for (InteractionHand hand : InteractionHand.values()) { 209 | ItemStack stack = mc.player.getItemInHand(hand); 210 | if (stack.getItem().equals(CameraMod.CAMERA.get()) && CameraMod.CAMERA.get().isActive(stack)) { 211 | return stack; 212 | } 213 | } 214 | return null; 215 | } 216 | 217 | private boolean isInCameraMode() { 218 | return getActiveCamera() != null; 219 | } 220 | 221 | } -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/CameraMod.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera; 2 | 3 | import com.mojang.serialization.Codec; 4 | import de.maxhenkel.camera.entities.ImageEntity; 5 | import de.maxhenkel.camera.gui.AlbumContainer; 6 | import de.maxhenkel.camera.gui.AlbumInventoryContainer; 7 | import de.maxhenkel.camera.items.AlbumItem; 8 | import de.maxhenkel.camera.items.CameraItem; 9 | import de.maxhenkel.camera.items.ImageFrameItem; 10 | import de.maxhenkel.camera.items.ImageItem; 11 | import de.maxhenkel.camera.net.*; 12 | import de.maxhenkel.corelib.CommonRegistry; 13 | import de.maxhenkel.corelib.codec.CodecUtils; 14 | import net.minecraft.core.UUIDUtil; 15 | import net.minecraft.core.component.DataComponentType; 16 | import net.minecraft.core.registries.BuiltInRegistries; 17 | import net.minecraft.network.codec.ByteBufCodecs; 18 | import net.minecraft.network.codec.StreamCodec; 19 | import net.minecraft.network.syncher.EntityDataSerializer; 20 | import net.minecraft.resources.Identifier; 21 | import net.minecraft.tags.ItemTags; 22 | import net.minecraft.tags.TagKey; 23 | import net.minecraft.util.Unit; 24 | import net.minecraft.world.entity.EntityType; 25 | import net.minecraft.world.entity.MobCategory; 26 | import net.minecraft.world.inventory.MenuType; 27 | import net.minecraft.world.item.Item; 28 | import net.minecraft.world.item.crafting.RecipeSerializer; 29 | import net.neoforged.bus.api.IEventBus; 30 | import net.neoforged.bus.api.SubscribeEvent; 31 | import net.neoforged.fml.common.EventBusSubscriber; 32 | import net.neoforged.fml.common.Mod; 33 | import net.neoforged.fml.config.ModConfig; 34 | import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; 35 | import net.neoforged.neoforge.common.extensions.IMenuTypeExtension; 36 | import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; 37 | import net.neoforged.neoforge.network.registration.PayloadRegistrar; 38 | import net.neoforged.neoforge.registries.DeferredHolder; 39 | import net.neoforged.neoforge.registries.DeferredRegister; 40 | import net.neoforged.neoforge.registries.NeoForgeRegistries; 41 | import org.apache.logging.log4j.LogManager; 42 | import org.apache.logging.log4j.Logger; 43 | 44 | import java.util.Optional; 45 | import java.util.UUID; 46 | 47 | @Mod(CameraMod.MODID) 48 | @EventBusSubscriber(modid = CameraMod.MODID) 49 | public class CameraMod { 50 | 51 | public static final String MODID = "camera"; 52 | public static final Logger LOGGER = LogManager.getLogger(MODID); 53 | 54 | public static PacketManager PACKET_MANAGER; 55 | 56 | private static final DeferredRegister.Items ITEM_REGISTER = DeferredRegister.createItems(MODID); 57 | 58 | public static final DeferredHolder FRAME_ITEM = ITEM_REGISTER.registerItem("image_frame", ImageFrameItem::new); 59 | public static final DeferredHolder CAMERA = ITEM_REGISTER.registerItem("camera", CameraItem::new); 60 | public static final DeferredHolder IMAGE = ITEM_REGISTER.registerItem("image", ImageItem::new); 61 | public static final DeferredHolder ALBUM = ITEM_REGISTER.registerItem("album", AlbumItem::new); 62 | 63 | private static final DeferredRegister> MENU_REGISTER = DeferredRegister.create(BuiltInRegistries.MENU, MODID); 64 | public static final DeferredHolder, MenuType> ALBUM_INVENTORY_CONTAINER = MENU_REGISTER.register("album_inventory", () -> IMenuTypeExtension.create((windowId, inv, data) -> new AlbumInventoryContainer(windowId, inv))); 65 | public static final DeferredHolder, MenuType> ALBUM_CONTAINER = MENU_REGISTER.register("album", () -> IMenuTypeExtension.create((windowId, inv, data) -> new AlbumContainer(windowId))); 66 | 67 | private static final DeferredRegister> ENTITY_REGISTER = DeferredRegister.create(BuiltInRegistries.ENTITY_TYPE, MODID); 68 | public static final DeferredHolder, EntityType> IMAGE_ENTITY_TYPE = ENTITY_REGISTER.register("image_frame", CameraMod::createImageEntityType); 69 | 70 | private static final DeferredRegister> RECIPE_SERIALIZER_REGISTER = DeferredRegister.create(BuiltInRegistries.RECIPE_SERIALIZER, MODID); 71 | public static final DeferredHolder, RecipeSerializer> IMAGE_CLONING_SERIALIZER = RECIPE_SERIALIZER_REGISTER.register("image_cloning", ImageCloningRecipe.ImageCloningSerializer::new); 72 | 73 | private static final DeferredRegister> DATA_COMPONENT_TYPE_REGISTER = DeferredRegister.create(BuiltInRegistries.DATA_COMPONENT_TYPE, CameraMod.MODID); 74 | public static final DeferredHolder, DataComponentType> IMAGE_DATA_COMPONENT = DATA_COMPONENT_TYPE_REGISTER.register("image", () -> DataComponentType.builder().persistent(ImageData.CODEC).networkSynchronized(ImageData.STREAM_CODEC).build()); 75 | public static final DeferredHolder, DataComponentType> ACTIVE_DATA_COMPONENT = DATA_COMPONENT_TYPE_REGISTER.register("active", () -> DataComponentType.builder().networkSynchronized(StreamCodec.unit(Unit.INSTANCE)).build()); 76 | public static final DeferredHolder, DataComponentType> SHADER_DATA_COMPONENT = DATA_COMPONENT_TYPE_REGISTER.register("shader", () -> DataComponentType.builder().persistent(Codec.STRING).networkSynchronized(ByteBufCodecs.STRING_UTF8).build()); 77 | 78 | private static final DeferredRegister> ENTITY_DATA_SERIALIZER_REGISTER = DeferredRegister.create(NeoForgeRegistries.ENTITY_DATA_SERIALIZERS, CameraMod.MODID); 79 | public static final DeferredHolder, EntityDataSerializer>> UUID_ENTITY_DATA_SERIALIZER = ENTITY_DATA_SERIALIZER_REGISTER.register("uuid", () -> EntityDataSerializer.forValueType(CodecUtils.optionalStreamCodecByteBuf(UUIDUtil.STREAM_CODEC))); 80 | 81 | public static TagKey IMAGE_PAPER = ItemTags.create(Identifier.fromNamespaceAndPath(CameraMod.MODID, "image_paper")); 82 | 83 | public static ServerConfig SERVER_CONFIG; 84 | public static ClientConfig CLIENT_CONFIG; 85 | 86 | public CameraMod(IEventBus eventBus) { 87 | eventBus.addListener(CreativeTabEvents::onCreativeModeTabBuildContents); 88 | 89 | SERVER_CONFIG = CommonRegistry.registerConfig(MODID, ModConfig.Type.SERVER, ServerConfig.class, true); 90 | CLIENT_CONFIG = CommonRegistry.registerConfig(MODID, ModConfig.Type.CLIENT, ClientConfig.class, true); 91 | 92 | ITEM_REGISTER.register(eventBus); 93 | MENU_REGISTER.register(eventBus); 94 | ENTITY_REGISTER.register(eventBus); 95 | RECIPE_SERIALIZER_REGISTER.register(eventBus); 96 | DATA_COMPONENT_TYPE_REGISTER.register(eventBus); 97 | ENTITY_DATA_SERIALIZER_REGISTER.register(eventBus); 98 | ModSounds.SOUND_REGISTER.register(eventBus); 99 | } 100 | 101 | @SubscribeEvent 102 | static void commonSetup(FMLCommonSetupEvent event) { 103 | PACKET_MANAGER = new PacketManager(); 104 | } 105 | 106 | private static EntityType createImageEntityType() { 107 | return CommonRegistry.registerEntity(CameraMod.MODID, "image_frame", MobCategory.MISC, ImageEntity.class, builder -> { 108 | builder.setTrackingRange(256) 109 | .setUpdateInterval(20) 110 | .setShouldReceiveVelocityUpdates(false) 111 | .sized(1F, 1F); 112 | }); 113 | } 114 | 115 | @SubscribeEvent 116 | static void onRegisterPayloadHandler(RegisterPayloadHandlersEvent event) { 117 | PayloadRegistrar registrar = event.registrar(MODID).versioned("0"); 118 | CommonRegistry.registerMessage(registrar, MessagePartialImage.class); 119 | CommonRegistry.registerMessage(registrar, MessageTakeImage.class); 120 | CommonRegistry.registerMessage(registrar, MessageRequestImage.class); 121 | CommonRegistry.registerMessage(registrar, MessageImage.class); 122 | CommonRegistry.registerMessage(registrar, MessageImageUnavailable.class); 123 | CommonRegistry.registerMessage(registrar, MessageSetShader.class); 124 | CommonRegistry.registerMessage(registrar, MessageDisableCameraMode.class); 125 | CommonRegistry.registerMessage(registrar, MessageResizeFrame.class); 126 | CommonRegistry.registerMessage(registrar, MessageRequestUploadCustomImage.class); 127 | CommonRegistry.registerMessage(registrar, MessageUploadCustomImage.class); 128 | CommonRegistry.registerMessage(registrar, MessageAlbumPage.class); 129 | CommonRegistry.registerMessage(registrar, MessageTakeBook.class); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/camera/entities/ImageRenderer.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.camera.entities; 2 | 3 | import com.mojang.blaze3d.platform.NativeImage; 4 | import com.mojang.blaze3d.vertex.PoseStack; 5 | import com.mojang.blaze3d.vertex.VertexConsumer; 6 | import com.mojang.math.Axis; 7 | import de.maxhenkel.camera.CameraMod; 8 | import de.maxhenkel.camera.TextureCache; 9 | import net.minecraft.client.Minecraft; 10 | import net.minecraft.client.renderer.*; 11 | import net.minecraft.client.renderer.entity.EntityRenderer; 12 | import net.minecraft.client.renderer.entity.EntityRendererProvider; 13 | import net.minecraft.client.renderer.rendertype.RenderTypes; 14 | import net.minecraft.client.renderer.state.CameraRenderState; 15 | import net.minecraft.client.renderer.texture.OverlayTexture; 16 | import net.minecraft.core.Direction; 17 | import net.minecraft.gizmos.GizmoStyle; 18 | import net.minecraft.gizmos.Gizmos; 19 | import net.minecraft.resources.Identifier; 20 | import net.minecraft.util.ARGB; 21 | import net.minecraft.world.phys.EntityHitResult; 22 | 23 | import javax.annotation.Nonnull; 24 | import java.util.UUID; 25 | 26 | public class ImageRenderer extends EntityRenderer { 27 | 28 | private static final Identifier DEFAULT_IMAGE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/images/default_image.png"); 29 | private static final Identifier EMPTY_IMAGE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/images/empty_image.png"); 30 | private static final Identifier FRAME_SIDE = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/images/frame_side.png"); 31 | private static final Identifier FRAME_BACK = Identifier.fromNamespaceAndPath(CameraMod.MODID, "textures/images/frame_back.png"); 32 | 33 | private static final float THICKNESS = 1F / 16F; 34 | public static final UUID DEFAULT_IMAGE_UUID = new UUID(0L, 0L); 35 | 36 | private static Minecraft mc; 37 | 38 | public ImageRenderer(EntityRendererProvider.Context context) { 39 | super(context); 40 | mc = Minecraft.getInstance(); 41 | } 42 | 43 | @Override 44 | public ImageEntityRenderState createRenderState() { 45 | return new ImageEntityRenderState(); 46 | } 47 | 48 | 49 | @Override 50 | public void submit(ImageEntityRenderState state, PoseStack stack, SubmitNodeCollector collector, CameraRenderState cameraRenderState) { 51 | super.submit(state, stack, collector, cameraRenderState); 52 | stack.pushPose(); 53 | stack.translate(-0.5D, 0D, -0.5D); 54 | submitImage(state.imageState, state.facing, state.frameWidth, state.frameHeight, state.light, stack, collector); 55 | stack.popPose(); 56 | submitBoundingBox(state, stack, collector); 57 | } 58 | 59 | public static void submitImage(ImageEntityRenderState.ImageState imageState, Direction facing, float width, float height, int light, PoseStack stack, SubmitNodeCollector collector) { 60 | stack.pushPose(); 61 | 62 | Identifier resourceLocation = imageState.resourceLocation(); 63 | float imageRatio = imageState.imageRatio(); 64 | boolean stretch = DEFAULT_IMAGE.equals(resourceLocation); 65 | 66 | rotate(facing, stack); 67 | 68 | float frameRatio = width / height; 69 | 70 | float ratio = imageRatio / frameRatio; 71 | 72 | float ratioX; 73 | float ratioY; 74 | 75 | if (stretch) { 76 | ratioX = 0F; 77 | ratioY = 0F; 78 | } else { 79 | if (ratio >= 1F) { 80 | ratioY = ((1F - 1F / ratio) / 2F) * height; 81 | ratioX = 0F; 82 | } else { 83 | ratioX = ((1F - ratio) / 2F) * width; 84 | ratioY = 0F; 85 | } 86 | } 87 | 88 | collector.submitCustomGeometry(stack, RenderTypes.entityCutout(resourceLocation), (pose, vertexConsumer) -> { 89 | // Front 90 | vertex(vertexConsumer, pose, 0F + ratioX, ratioY, THICKNESS, 0F, 1F, light); 91 | vertex(vertexConsumer, pose, width - ratioX, ratioY, THICKNESS, 1F, 1F, light); 92 | vertex(vertexConsumer, pose, width - ratioX, height - ratioY, THICKNESS, 1F, 0F, light); 93 | vertex(vertexConsumer, pose, ratioX, height - ratioY, THICKNESS, 0F, 0F, light); 94 | }); 95 | 96 | collector.submitCustomGeometry(stack, RenderTypes.entityCutout(FRAME_SIDE), (pose, vertexConsumer) -> { 97 | //Left 98 | vertex(vertexConsumer, pose, 0F + ratioX, 0F + ratioY, 0F, 1F, 0F + ratioY, light); 99 | vertex(vertexConsumer, pose, 0F + ratioX, 0F + ratioY, THICKNESS, 1F - THICKNESS, 0F + ratioY, light); 100 | vertex(vertexConsumer, pose, 0F + ratioX, height - ratioY, THICKNESS, 1F - THICKNESS, 1F - ratioY, light); 101 | vertex(vertexConsumer, pose, 0F + ratioX, height - ratioY, 0F, 1F, 1F - ratioY, light); 102 | 103 | //Right 104 | vertex(vertexConsumer, pose, width - ratioX, 0F + ratioY, 0F, 0F, 0F + ratioY, light); 105 | vertex(vertexConsumer, pose, width - ratioX, height - ratioY, 0F, 0F, 1F - ratioY, light); 106 | vertex(vertexConsumer, pose, width - ratioX, height - ratioY, THICKNESS, THICKNESS, 1F - ratioY, light); 107 | vertex(vertexConsumer, pose, width - ratioX, 0F + ratioY, THICKNESS, THICKNESS, 0F + ratioY, light); 108 | 109 | //Top 110 | vertex(vertexConsumer, pose, 0F + ratioX, height - ratioY, 0F, 0F + ratioX, 1F, light); 111 | vertex(vertexConsumer, pose, 0F + ratioX, height - ratioY, THICKNESS, 0F + ratioX, 1F - THICKNESS, light); 112 | vertex(vertexConsumer, pose, width - ratioX, height - ratioY, THICKNESS, 1F - ratioX, 1F - THICKNESS, light); 113 | vertex(vertexConsumer, pose, width - ratioX, height - ratioY, 0F, 1F - ratioX, 1F, light); 114 | 115 | //Bottom 116 | vertex(vertexConsumer, pose, 0F + ratioX, 0F + ratioY, 0F, 0F + ratioX, 0F, light); 117 | vertex(vertexConsumer, pose, width - ratioX, 0F + ratioY, 0F, 1F - ratioX, 0F, light); 118 | vertex(vertexConsumer, pose, width - ratioX, 0F + ratioY, THICKNESS, 1F - ratioX, THICKNESS, light); 119 | vertex(vertexConsumer, pose, 0F + ratioX, 0F + ratioY, THICKNESS, 0F + ratioX, THICKNESS, light); 120 | }); 121 | 122 | collector.submitCustomGeometry(stack, RenderTypes.entityCutout(FRAME_BACK), (pose, vertexConsumer) -> { 123 | //Back 124 | vertex(vertexConsumer, pose, width - ratioX, 0F + ratioY, 0F, 1F - ratioX, 0F + ratioY, light); 125 | vertex(vertexConsumer, pose, 0F + ratioX, 0F + ratioY, 0F, 0F + ratioX, 0F + ratioY, light); 126 | vertex(vertexConsumer, pose, 0F + ratioX, height - ratioY, 0F, 0F + ratioX, 1F - ratioY, light); 127 | vertex(vertexConsumer, pose, width - ratioX, height - ratioY, 0F, 1F - ratioX, 1F - ratioY, light); 128 | }); 129 | 130 | stack.popPose(); 131 | } 132 | 133 | @Override 134 | public void extractRenderState(ImageEntity image, ImageEntityRenderState state, float partialTicks) { 135 | super.extractRenderState(image, state, partialTicks); 136 | 137 | state.imageEntityUUID = image.getUUID(); 138 | 139 | state.frameWidth = image.getFrameWidth(); 140 | state.frameHeight = image.getFrameHeight(); 141 | state.facing = image.getFacing(); 142 | state.imageState = extractImageState(image.getImageUUID().orElse(DEFAULT_IMAGE_UUID)); 143 | state.light = LevelRenderer.getLightColor(image.level(), image.getCenterPosition()); 144 | state.imageBoundingBox = image.getBoundingBox(); 145 | } 146 | 147 | public static ImageEntityRenderState.ImageState extractImageState(@Nonnull UUID imageId) { 148 | Identifier resourceLocation; 149 | float imageRatio; 150 | if (DEFAULT_IMAGE_UUID.equals(imageId)) { 151 | resourceLocation = DEFAULT_IMAGE; 152 | imageRatio = 1.5F; 153 | } else { 154 | Identifier rl = TextureCache.instance().getImage(imageId); 155 | if (rl != null) { 156 | resourceLocation = rl; 157 | NativeImage nativeImage = TextureCache.instance().getNativeImage(imageId); 158 | imageRatio = (float) nativeImage.getWidth() / (float) nativeImage.getHeight(); 159 | } else { 160 | resourceLocation = DEFAULT_IMAGE; 161 | imageRatio = 1.5F; 162 | } 163 | } 164 | return new ImageEntityRenderState.ImageState(imageId, imageRatio, resourceLocation); 165 | } 166 | 167 | private static void vertex(VertexConsumer builder, PoseStack.Pose pose, float x, float y, float z, float u, float v, int light) { 168 | builder.addVertex(pose.pose(), x, y, z) 169 | .setColor(1F, 1F, 1F, 1F) 170 | .setUv(u, v) 171 | .setOverlay(OverlayTexture.NO_OVERLAY) 172 | .setLight(light) 173 | .setNormal(pose, 0F, 0F, -1F); 174 | } 175 | 176 | private static void submitBoundingBox(ImageEntityRenderState state, PoseStack stack, SubmitNodeCollector collector) { 177 | if (!(mc.hitResult instanceof EntityHitResult entityHitResult) || !entityHitResult.getEntity().getUUID().equals(state.imageEntityUUID)) { 178 | return; 179 | } 180 | if (mc.options.hideGui) { 181 | return; 182 | } 183 | Gizmos.cuboid( 184 | state.imageBoundingBox, 185 | GizmoStyle.stroke(ARGB.colorFromFloat(0.4F, 0F, 0F, 0F)), 186 | false 187 | ); 188 | } 189 | 190 | public static void rotate(Direction facing, PoseStack matrixStack) { 191 | switch (facing) { 192 | case NORTH: 193 | matrixStack.translate(1D, 0D, 1D); 194 | matrixStack.mulPose(Axis.YP.rotationDegrees(180F)); 195 | break; 196 | case SOUTH: 197 | break; 198 | case EAST: 199 | matrixStack.translate(0D, 0D, 1D); 200 | matrixStack.mulPose(Axis.YP.rotationDegrees(90F)); 201 | break; 202 | case WEST: 203 | matrixStack.translate(1D, 0D, 0D); 204 | matrixStack.mulPose(Axis.YP.rotationDegrees(270F)); 205 | break; 206 | } 207 | } 208 | 209 | } 210 | --------------------------------------------------------------------------------