diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/resources/builder/layout/item/ItemModelDefinitionBuilder.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/resources/builder/layout/item/ItemModelDefinitionBuilder.kt index 5ecda7386d9..0470967bccb 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/resources/builder/layout/item/ItemModelDefinitionBuilder.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/resources/builder/layout/item/ItemModelDefinitionBuilder.kt @@ -1,5 +1,7 @@ package xyz.xenondevs.nova.resources.builder.layout.item +import org.joml.Vector3d +import xyz.xenondevs.commons.collections.enumMapOf import xyz.xenondevs.nova.registry.RegistryElementBuilderDsl import xyz.xenondevs.nova.resources.ResourcePath import xyz.xenondevs.nova.resources.ResourceType @@ -9,9 +11,14 @@ import xyz.xenondevs.nova.resources.builder.data.EmptyItemModel import xyz.xenondevs.nova.resources.builder.data.ItemModel import xyz.xenondevs.nova.resources.builder.data.ItemModelDefinition import xyz.xenondevs.nova.resources.builder.data.SpecialItemModel.SpecialModel +import xyz.xenondevs.nova.resources.builder.data.TintSource import xyz.xenondevs.nova.resources.builder.layout.ModelSelectorScope import xyz.xenondevs.nova.resources.builder.layout.block.BlockModelSelectorScope +import xyz.xenondevs.nova.resources.builder.model.Model import xyz.xenondevs.nova.resources.builder.model.ModelBuilder +import xyz.xenondevs.nova.resources.builder.task.model.ModelContent +import xyz.xenondevs.nova.ui.menu.Canvas +import java.awt.Color @RegistryElementBuilderDsl class ItemModelDefinitionBuilder internal constructor( @@ -195,4 +202,80 @@ sealed class ItemModelCreationScope( } } + /** + * Creates a canvas model, which is an item model with a flat texture where each pixel is individually + * addressable using the `colors` part of the `minecraft:custom_model_data` component, where + * the pixel at (x, y) is found under the index `y * width + x` and (0, 0) is the top-left pixel. + * + * The maximum [width] and [height] are `161 - |offsetX|` and `161 - |offsetY|` respectively. + * (Consider using multiple smaller canvases instead of a single large one to reduce the resource pack size.) + * + * @param width The width of the canvas, in pixels. + * @param height The height of the canvas, in pixels. + * @param offsetX The x offset of the canvas texture to the item's center, pixels. + * @param offsetY The y offset of the canvas texture to the item's center, pixels. + * @param scale The size of the pixels in the canvas texture. + * A scale of 2 means each pixel is 2x2 pixels in game (assuming a client-side gui-scale of 1). + * Defaults to 1. + * + * @see Canvas + */ + fun canvasModel( + width: Int, height: Int, + offsetX: Double = 0.0, offsetY: Double = 0.0, + scale: Double = 1.0 + ): ItemModel = select(SelectItemModelProperty.DisplayContext) { + // the actual width and height of the canvas, in pixel models needed, takes scale into account + val actualWidth = (width / scale).toInt() + val actualHeight = (height / scale).toInt() + + // the individual pixel models apply a display scale of 4, so actualScale counteracts this with 0.25 + val actualScale = 0.25 * scale + + // parent model for all pixels with this scale + val parentModel = Model( + parent = ResourcePath(ResourceType.Model, "nova", "item/canvas_pixel"), + elements = listOf( + Model.Element( + from = Vector3d(8.0, 8.0 - actualScale, 0.0), + to = Vector3d(8.0 + actualScale, 8.0, 0.0), + faces = enumMapOf( + Model.Direction.SOUTH to Model.Element.Face( + texture = "#0", + tintIndex = 0 + ) + ) + ) + ) + ) + val parentModelId = resourcePackBuilder.getHolder().getOrPutGenerated(parentModel) + + fallback = empty() + case[DisplayContext.GUI] = composite { + for (y in 0.. { + + private val items = ArrayList() + private var supplierIndex = 0 + + /** + * Creates a new [Canvas] with the [DefaultGuiItems.CANVAS] (18x18 px) item. + * + * @param image The image that is used to fill the canvas. Will be read from every time [notifyWindows] is called. + */ + constructor(image: BufferedImage) : this(DefaultGuiItems.CANVAS, 18, image) + + init { + require(image.type == BufferedImage.TYPE_INT_ARGB) { "Image needs to be TYPE_INT_ARGB" } + require(image.height % itemResolution == 0) { "Image height needs to be divisible by $itemResolution" } + require(image.width % itemResolution == 0) { "Image width needs to be divisible by $itemResolution" } + + for (y in 0..<(image.height / itemResolution)) { + for (x in 0..<(image.width / itemResolution)) { + items += CanvasPart(x, y) + } + } + } + + override fun get(): Item { + return items[supplierIndex++] + } + + /** + * [Notifies][Item.notifyWindows] all windows of all items of this canvas. + */ + fun notifyWindows() { + items.notifyWindows() + } + + /** + * Modifies the [itemBuilder] for the canvas part at the given [x] and [y] coordinates, + * which will be displayed to [viewer]. + */ + open fun modifyItemBuilder(x: Int, y: Int, viewer: Player, itemBuilder: ItemBuilder) = Unit + + /** + * Handles a [click] on the canvas part at the given [x] and [y] coordinates. + */ + open fun handleClick(x: Int, y: Int, click: Click) = Unit + + private inner class CanvasPart(private val x: Int, private val y: Int) : AbstractItem() { + + private val colors = IntArray(itemResolution * itemResolution) + + override fun getItemProvider(viewer: Player): ItemProvider { + // read colors from image + image.getRGB( + x * itemResolution, y * itemResolution, + itemResolution, itemResolution, + colors, + 0, + itemResolution + ) + + // write colors to item stack + val itemStack = canvasItem.clientsideProvider.get().unwrap().copy() + itemStack.set( + DataComponents.CUSTOM_MODEL_DATA, + CustomModelData( + emptyList(), + emptyList(), + emptyList(), + IntArrayList(colors) + ) + ) + + val builder = ItemBuilder(itemStack.asBukkitMirror()) + modifyItemBuilder(x, y, viewer, builder) + return builder + } + + override fun handleClick(clickType: ClickType, player: Player, click: Click) { + handleClick(x, y, click) + } + + } + +} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/data/ImageUtils.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/data/ImageUtils.kt index 5753813e7e6..b951cce523e 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/data/ImageUtils.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/util/data/ImageUtils.kt @@ -120,7 +120,7 @@ internal object ImageUtils { @JvmStatic fun createImageFromArgbRaster(width: Int, raster: IntArray): BufferedImage { // https://stackoverflow.com/questions/14416107/int-array-to-bufferedimage - val sm = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, width, 16, ARGB_BIT_MASKS) + val sm = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, width, raster.size / width, ARGB_BIT_MASKS) val db = DataBufferInt(raster, raster.size) val wr = Raster.createWritableRaster(sm, db, Point()) return BufferedImage(ColorModel.getRGBdefault(), wr, false, null) diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/item/DefaultGuiItems.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/item/DefaultGuiItems.kt index ecbc9401500..41bfbfd06e1 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/item/DefaultGuiItems.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/item/DefaultGuiItems.kt @@ -4,6 +4,7 @@ import net.kyori.adventure.key.Key import xyz.xenondevs.nova.initialize.InternalInit import xyz.xenondevs.nova.initialize.InternalInitStage import xyz.xenondevs.nova.resources.builder.ResourcePackBuilder +import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelCreationScope import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelDefinitionBuilder import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelSelectorScope import xyz.xenondevs.nova.util.data.writeImage @@ -97,6 +98,17 @@ object DefaultGuiItems { val STOPWATCH = guiItem("stopwatch") // + /** + * An 18x18 scale 1 [canvas][ItemModelCreationScope.canvasModel]. + */ + val CANVAS = item("gui/canvas") { + hidden(true) + name(null) + modelDefinition { + model = canvasModel(18, 18) + } + } + // // legacy InvUI gui items val TP_LINE_CORNER_BOTTOM_LEFT = tpGuiItem("line/corner_bottom_left", null, true) diff --git a/nova/src/main/resources/assets/nova/models/item/canvas_pixel.json b/nova/src/main/resources/assets/nova/models/item/canvas_pixel.json new file mode 100644 index 00000000000..5eddd896057 --- /dev/null +++ b/nova/src/main/resources/assets/nova/models/item/canvas_pixel.json @@ -0,0 +1,6 @@ +{ + "parent": "nova:item/gui_item", + "textures": { + "0": "nova:item/white" + } +} \ No newline at end of file diff --git a/nova/src/main/resources/assets/nova/textures/item/white.png b/nova/src/main/resources/assets/nova/textures/item/white.png new file mode 100644 index 00000000000..356fd0c9284 Binary files /dev/null and b/nova/src/main/resources/assets/nova/textures/item/white.png differ