aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-03-01 21:31:48 +0100
committerLinnea Gräf <nea@nea.moe>2024-03-02 19:43:00 +0100
commit3bfec3033e9d905514d5c1c6c62953c2a1646af0 (patch)
treeafee0b3c20d0bdef9780e5fddb368c9d58fb3c70 /src/main/kotlin/moe/nea
parentf28dee0ef3a0dd4a0819a3d3a1c800a83a0f07f5 (diff)
downloadFirmament-3bfec3033e9d905514d5c1c6c62953c2a1646af0.tar.gz
Firmament-3bfec3033e9d905514d5c1c6c62953c2a1646af0.tar.bz2
Firmament-3bfec3033e9d905514d5c1c6c62953c2a1646af0.zip
Add mob drop viewer to item list
Diffstat (limited to 'src/main/kotlin/moe/nea')
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/rome.kt41
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/EntityModifier.kt14
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/EntityRenderer.kt202
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/EntityWidget.kt40
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/FakeWorld.kt491
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/GuiPlayer.kt55
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyAge.kt30
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyCharged.kt19
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyEquipment.kt59
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyHorse.kt66
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyInvisible.kt18
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyName.kt19
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyPlayerSkin.kt32
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyRiding.kt20
-rw-r--r--src/main/kotlin/moe/nea/firmament/gui/entity/ModifyWither.kt25
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt4
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt8
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/SkyblockCraftingRecipeDynamicGenerator.kt7
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/recipes/SBMobDropRecipe.kt113
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/RepoModResourcePack.kt101
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/ItemUtil.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/LoadResource.kt25
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/assertions.kt1
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt40
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/render/TranslatedScissors.kt27
25 files changed, 1447 insertions, 12 deletions
diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
index 91bdf47..7df39b3 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -52,7 +53,12 @@ fun firmamentCommand() = literal("firmament") {
val configObj = AllConfigsGui.allConfigs.find { it.name == config }
if (configObj == null) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.command.toggle.no-config-found", config))
+ source.sendFeedback(
+ Text.stringifiedTranslatable(
+ "firmament.command.toggle.no-config-found",
+ config
+ )
+ )
return@thenExecute
}
val propertyObj = configObj.allOptions[property]
@@ -72,9 +78,11 @@ fun firmamentCommand() = literal("firmament") {
propertyObj.value = !propertyObj.value
configObj.save()
source.sendFeedback(
- Text.stringifiedTranslatable("firmament.command.toggle.toggled",configObj.labelText,
- propertyObj.labelText,
- Text.translatable("firmament.toggle.${propertyObj.value}"))
+ Text.stringifiedTranslatable(
+ "firmament.command.toggle.toggled", configObj.labelText,
+ propertyObj.labelText,
+ Text.translatable("firmament.toggle.${propertyObj.value}")
+ )
)
}
}
@@ -144,22 +152,37 @@ fun firmamentCommand() = literal("firmament") {
Text.stringifiedTranslatable("firmament.price.bazaar.productid", bazaarData.productId.bazaarId)
)
source.sendFeedback(
- Text.stringifiedTranslatable("firmament.price.bazaar.buy.price", FirmFormatters.formatCurrency(bazaarData.quickStatus.buyPrice, 1))
+ Text.stringifiedTranslatable(
+ "firmament.price.bazaar.buy.price",
+ FirmFormatters.formatCurrency(bazaarData.quickStatus.buyPrice, 1)
+ )
)
source.sendFeedback(
- Text.stringifiedTranslatable("firmament.price.bazaar.buy.order", bazaarData.quickStatus.buyOrders)
+ Text.stringifiedTranslatable(
+ "firmament.price.bazaar.buy.order",
+ bazaarData.quickStatus.buyOrders
+ )
)
source.sendFeedback(
- Text.stringifiedTranslatable("firmament.price.bazaar.sell.price", FirmFormatters.formatCurrency(bazaarData.quickStatus.sellPrice, 1))
+ Text.stringifiedTranslatable(
+ "firmament.price.bazaar.sell.price",
+ FirmFormatters.formatCurrency(bazaarData.quickStatus.sellPrice, 1)
+ )
)
source.sendFeedback(
- Text.stringifiedTranslatable("firmament.price.bazaar.sell.order", bazaarData.quickStatus.sellOrders)
+ Text.stringifiedTranslatable(
+ "firmament.price.bazaar.sell.order",
+ bazaarData.quickStatus.sellOrders
+ )
)
}
val lowestBin = HypixelStaticData.lowestBin[itemName]
if (lowestBin != null) {
source.sendFeedback(
- Text.stringifiedTranslatable("firmament.price.lowestbin", FirmFormatters.formatCurrency(lowestBin, 1))
+ Text.stringifiedTranslatable(
+ "firmament.price.lowestbin",
+ FirmFormatters.formatCurrency(lowestBin, 1)
+ )
)
}
}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/EntityModifier.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/EntityModifier.kt
new file mode 100644
index 0000000..fae3a4b
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/EntityModifier.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+
+fun interface EntityModifier {
+ fun apply(entity: LivingEntity, info: JsonObject): LivingEntity
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/EntityRenderer.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/EntityRenderer.kt
new file mode 100644
index 0000000..d645e5b
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/EntityRenderer.kt
@@ -0,0 +1,202 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import org.apache.logging.log4j.LogManager
+import org.joml.Quaternionf
+import org.joml.Vector3f
+import kotlin.math.atan
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.ingame.InventoryScreen
+import net.minecraft.entity.Entity
+import net.minecraft.entity.EntityType
+import net.minecraft.entity.LivingEntity
+import net.minecraft.util.Identifier
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.assertNotNullOr
+import moe.nea.firmament.util.iterate
+import moe.nea.firmament.util.openFirmamentResource
+import moe.nea.firmament.util.render.enableScissorWithTranslation
+
+object EntityRenderer {
+ val fakeWorld = FakeWorld()
+ private fun <T : Entity> t(entityType: EntityType<T>): () -> T {
+ return { entityType.create(fakeWorld)!! }
+ }
+
+ val entityIds: Map<String, () -> LivingEntity> = mapOf(
+ "Zombie" to t(EntityType.ZOMBIE),
+ "Chicken" to t(EntityType.CHICKEN),
+ "Slime" to t(EntityType.SLIME),
+ "Wolf" to t(EntityType.WOLF),
+ "Skeleton" to t(EntityType.SKELETON),
+ "Creeper" to t(EntityType.CREEPER),
+ "Ocelot" to t(EntityType.OCELOT),
+ "Blaze" to t(EntityType.BLAZE),
+ "Rabbit" to t(EntityType.RABBIT),
+ "Sheep" to t(EntityType.SHEEP),
+ "Horse" to t(EntityType.HORSE),
+ "Eisengolem" to t(EntityType.IRON_GOLEM),
+ "Silverfish" to t(EntityType.SILVERFISH),
+ "Witch" to t(EntityType.WITCH),
+ "Endermite" to t(EntityType.ENDERMITE),
+ "Snowman" to t(EntityType.SNOW_GOLEM),
+ "Villager" to t(EntityType.VILLAGER),
+ "Guardian" to t(EntityType.GUARDIAN),
+ "ArmorStand" to t(EntityType.ARMOR_STAND),
+ "Squid" to t(EntityType.SQUID),
+ "Bat" to t(EntityType.BAT),
+ "Spider" to t(EntityType.SPIDER),
+ "CaveSpider" to t(EntityType.CAVE_SPIDER),
+ "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN),
+ "Ghast" to t(EntityType.GHAST),
+ "MagmaCube" to t(EntityType.MAGMA_CUBE),
+ "Wither" to t(EntityType.WITHER),
+ "Enderman" to t(EntityType.ENDERMAN),
+ "Mooshroom" to t(EntityType.MOOSHROOM),
+ "WitherSkeleton" to t(EntityType.WITHER_SKELETON),
+ "Cow" to t(EntityType.COW),
+ "Dragon" to t(EntityType.ENDER_DRAGON),
+ "Player" to { makeGuiPlayer(fakeWorld) },
+ "Pig" to t(EntityType.PIG),
+ "Giant" to t(EntityType.GIANT),
+ )
+ val entityModifiers: Map<String, EntityModifier> = mapOf(
+ "playerdata" to ModifyPlayerSkin,
+ "equipment" to ModifyEquipment,
+ "riding" to ModifyRiding,
+ "charged" to ModifyCharged,
+ "witherdata" to ModifyWither,
+ "invisible" to ModifyInvisible,
+ "age" to ModifyAge,
+ "horse" to ModifyHorse,
+ "name" to ModifyName,
+ )
+
+ val logger = LogManager.getLogger("Firmament.Entity")
+ fun applyModifiers(entityId: String, modifiers: List<JsonObject>): LivingEntity? {
+ val entityType = assertNotNullOr(entityIds[entityId]) {
+ logger.error("Could not create entity with id $entityId")
+ return null
+ }
+ var entity = entityType()
+ for (modifierJson in modifiers) {
+ val modifier = assertNotNullOr(modifierJson["type"]?.asString?.let(entityModifiers::get)) {
+ logger.error("Unknown modifier $modifierJson")
+ return null
+ }
+ entity = modifier.apply(entity, modifierJson)
+ }
+ return entity
+ }
+
+ fun constructEntity(info: JsonObject): LivingEntity? {
+ val modifiers = (info["modifiers"] as JsonArray?)?.map { it.asJsonObject } ?: emptyList()
+ val entityType = assertNotNullOr(info["entity"]?.asString) {
+ logger.error("Missing entity type on entity object")
+ return null
+ }
+ return applyModifiers(entityType, modifiers)
+ }
+
+ private val gson = Gson()
+ fun constructEntity(location: Identifier): LivingEntity? {
+ return constructEntity(
+ gson.fromJson(
+ location.openFirmamentResource().bufferedReader(), JsonObject::class.java
+ )
+ )
+ }
+
+ fun renderEntity(
+ entity: LivingEntity,
+ renderContext: DrawContext,
+ posX: Int,
+ posY: Int,
+ mouseX: Float,
+ mouseY: Float
+ ) {
+ var bottomOffset = 0.0F
+ var currentEntity = entity
+ val maxSize = entity.iterate { it.firstPassenger as? LivingEntity }
+ .map { it.height }
+ .sum()
+ while (true) {
+ currentEntity.age = MC.player?.age ?: 0
+ drawEntity(
+ renderContext,
+ posX,
+ posY,
+ posX + 50,
+ posY + 80,
+ (2F / maxSize * 30).toInt(),
+ -bottomOffset,
+ mouseX,
+ mouseY,
+ currentEntity
+ )
+ val next = currentEntity.firstPassenger as? LivingEntity ?: break
+ bottomOffset += currentEntity.getPassengerRidingPos(next).y.toFloat() * 0.75F
+ currentEntity = next
+ }
+ }
+
+
+ fun drawEntity(
+ context: DrawContext,
+ x1: Int,
+ y1: Int,
+ x2: Int,
+ y2: Int,
+ size: Int,
+ bottomOffset: Float,
+ mouseX: Float,
+ mouseY: Float,
+ entity: LivingEntity
+ ) {
+ context.enableScissorWithTranslation(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat())
+ val centerX = (x1 + x2) / 2f
+ val centerY = (y1 + y2) / 2f
+ val targetYaw = atan(((centerX - mouseX) / 40.0f).toDouble()).toFloat()
+ val targetPitch = atan(((centerY - mouseY) / 40.0f).toDouble()).toFloat()
+ val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat())
+ val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180))
+ rotateToFaceTheFront.mul(rotateToFaceTheCamera)
+ val oldBodyYaw = entity.bodyYaw
+ val oldYaw = entity.yaw
+ val oldPitch = entity.pitch
+ val oldPrevHeadYaw = entity.prevHeadYaw
+ val oldHeadYaw = entity.headYaw
+ entity.bodyYaw = 180.0f + targetYaw * 20.0f
+ entity.yaw = 180.0f + targetYaw * 40.0f
+ entity.pitch = -targetPitch * 20.0f
+ entity.headYaw = entity.yaw
+ entity.prevHeadYaw = entity.yaw
+ val vector3f = Vector3f(0.0f, entity.height / 2.0f + bottomOffset, 0.0f)
+ InventoryScreen.drawEntity(
+ context,
+ centerX,
+ centerY,
+ size,
+ vector3f,
+ rotateToFaceTheFront,
+ rotateToFaceTheCamera,
+ entity
+ )
+ entity.bodyYaw = oldBodyYaw
+ entity.yaw = oldYaw
+ entity.pitch = oldPitch
+ entity.prevHeadYaw = oldPrevHeadYaw
+ entity.headYaw = oldHeadYaw
+ context.disableScissor()
+ }
+
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/EntityWidget.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/EntityWidget.kt
new file mode 100644
index 0000000..42fa485
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/EntityWidget.kt
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.Element
+import net.minecraft.entity.LivingEntity
+
+class EntityWidget(val entity: LivingEntity, val point: Point) : WidgetWithBounds() {
+ override fun children(): List<Element> {
+ return emptyList()
+ }
+
+ var hasErrored = false
+
+ override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ try {
+ if (!hasErrored)
+ EntityRenderer.renderEntity(entity, context, point.x, point.y, mouseX.toFloat(), mouseY.toFloat())
+ } catch (ex: Exception) {
+ EntityRenderer.logger.error("Failed to render constructed entity: $entity", ex)
+ hasErrored = true
+ }
+ if (hasErrored) {
+ context.fill(point.x, point.y, point.x + 50, point.y + 80, 0xFFAA2222.toInt())
+ }
+ }
+
+ override fun getBounds(): Rectangle {
+ return Rectangle(point, Dimension(50, 80))
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/FakeWorld.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/FakeWorld.kt
new file mode 100644
index 0000000..4cdfc45
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/FakeWorld.kt
@@ -0,0 +1,491 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.mojang.datafixers.util.Pair
+import com.mojang.serialization.Lifecycle
+import java.util.*
+import java.util.function.BooleanSupplier
+import java.util.function.Consumer
+import java.util.stream.Stream
+import kotlin.jvm.optionals.getOrNull
+import kotlin.streams.asSequence
+import net.minecraft.block.Block
+import net.minecraft.block.BlockState
+import net.minecraft.client.world.ClientWorld
+import net.minecraft.entity.Entity
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.fluid.Fluid
+import net.minecraft.item.map.MapState
+import net.minecraft.recipe.RecipeManager
+import net.minecraft.registry.BuiltinRegistries
+import net.minecraft.registry.DynamicRegistryManager
+import net.minecraft.registry.Registry
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.registry.RegistryWrapper
+import net.minecraft.registry.entry.RegistryEntry
+import net.minecraft.registry.entry.RegistryEntryList
+import net.minecraft.registry.entry.RegistryEntryOwner
+import net.minecraft.registry.tag.TagKey
+import net.minecraft.resource.featuretoggle.FeatureFlag
+import net.minecraft.resource.featuretoggle.FeatureFlags
+import net.minecraft.resource.featuretoggle.FeatureSet
+import net.minecraft.scoreboard.Scoreboard
+import net.minecraft.sound.SoundCategory
+import net.minecraft.sound.SoundEvent
+import net.minecraft.util.Identifier
+import net.minecraft.util.TypeFilter
+import net.minecraft.util.function.LazyIterationConsumer
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Box
+import net.minecraft.util.math.ChunkPos
+import net.minecraft.util.math.Direction
+import net.minecraft.util.math.Vec3d
+import net.minecraft.util.math.random.Random
+import net.minecraft.util.profiler.DummyProfiler
+import net.minecraft.world.BlockView
+import net.minecraft.world.Difficulty
+import net.minecraft.world.GameRules
+import net.minecraft.world.MutableWorldProperties
+import net.minecraft.world.World
+import net.minecraft.world.biome.Biome
+import net.minecraft.world.biome.BiomeKeys
+import net.minecraft.world.chunk.Chunk
+import net.minecraft.world.chunk.ChunkManager
+import net.minecraft.world.chunk.ChunkStatus
+import net.minecraft.world.chunk.EmptyChunk
+import net.minecraft.world.chunk.light.LightingProvider
+import net.minecraft.world.entity.EntityLookup
+import net.minecraft.world.event.GameEvent
+import net.minecraft.world.tick.OrderedTick
+import net.minecraft.world.tick.QueryableTickScheduler
+import net.minecraft.world.tick.TickManager
+
+fun <T> makeRegistry(registryWrapper: RegistryWrapper.Impl<T>, key: RegistryKey<out Registry<T>>): Registry<T> {
+ val inverseLookup = registryWrapper.streamEntries()
+ .asSequence().map { it.value() to it.registryKey() }
+ .toMap()
+ val idLookup = registryWrapper.streamEntries()
+ .asSequence()
+ .map { it.registryKey() }
+ .withIndex()
+ .associate { it.value to it.index }
+ val map = registryWrapper.streamEntries().asSequence().map { it.registryKey() to it.value() }.toMap(mutableMapOf())
+ val inverseIdLookup = idLookup.asIterable().associate { (k, v) -> v to k }
+ return object : Registry<T> {
+ override fun get(key: RegistryKey<T>?): T? {
+ return registryWrapper.getOptional(key).getOrNull()?.value()
+ }
+
+ override fun iterator(): MutableIterator<T> {
+ return object : MutableIterator<T> {
+ val iterator = registryWrapper.streamEntries().iterator()
+ override fun hasNext(): Boolean {
+ return iterator.hasNext()
+ }
+
+ override fun next(): T {
+ return iterator.next().value()
+ }
+
+ override fun remove() {
+ TODO("Not yet implemented")
+ }
+ }
+ }
+
+ override fun getRawId(value: T?): Int {
+ return idLookup[inverseLookup[value ?: return -1] ?: return -1] ?: return -1
+ }
+
+ override fun get(id: Identifier?): T? {
+ return get(RegistryKey.of(key, id))
+ }
+
+ override fun get(index: Int): T? {
+ return get(inverseIdLookup[index] ?: return null)
+ }
+
+ override fun size(): Int {
+ return idLookup.size
+ }
+
+ override fun getKey(): RegistryKey<out Registry<T>> {
+ return key
+ }
+
+ override fun getLifecycle(): Lifecycle {
+ return Lifecycle.stable()
+ }
+
+ override fun getIds(): MutableSet<Identifier> {
+ return idLookup.keys.mapTo(mutableSetOf()) { it.value }
+ }
+
+ override fun getEntrySet(): MutableSet<MutableMap.MutableEntry<RegistryKey<T>, T>> {
+ return map.entries
+ }
+
+ override fun getKeys(): MutableSet<RegistryKey<T>> {
+ return map.keys
+ }
+
+ override fun getRandom(random: Random?): Optional<RegistryEntry.Reference<T>> {
+ return registryWrapper.streamEntries().findFirst()
+ }
+
+ override fun containsId(id: Identifier?): Boolean {
+ return idLookup.containsKey(RegistryKey.of(key, id ?: return false))
+ }
+
+ override fun freeze(): Registry<T> {
+ return this
+ }
+
+ override fun getEntry(rawId: Int): Optional<RegistryEntry.Reference<T>> {
+ val x = inverseIdLookup[rawId] ?: return Optional.empty()
+ return Optional.of(RegistryEntry.Reference.standAlone(registryWrapper, x))
+ }
+
+ override fun streamEntries(): Stream<RegistryEntry.Reference<T>> {
+ return registryWrapper.streamEntries()
+ }
+
+ override fun streamTagsAndEntries(): Stream<Pair<TagKey<T>, RegistryEntryList.Named<T>>> {
+ return streamTags().map { Pair(it, getOrCreateEntryList(it)) }
+ }
+
+ override fun streamTags(): Stream<TagKey<T>> {
+ return registryWrapper.streamTagKeys()
+ }
+
+ override fun clearTags() {
+ }
+
+ override fun getEntryOwner(): RegistryEntryOwner<T> {
+ return registryWrapper
+ }
+
+ override fun getReadOnlyWrapper(): RegistryWrapper.Impl<T> {
+ return registryWrapper
+ }
+
+ override fun populateTags(tagEntries: MutableMap<TagKey<T>, MutableList<RegistryEntry<T>>>?) {
+ }
+
+ override fun getOrCreateEntryList(tag: TagKey<T>?): RegistryEntryList.Named<T> {
+ return getEntryList(tag).orElseGet { RegistryEntryList.of(registryWrapper, tag) }
+ }
+
+ override fun getEntryList(tag: TagKey<T>?): Optional<RegistryEntryList.Named<T>> {
+ return registryWrapper.getOptional(tag ?: return Optional.empty())
+ }
+
+ override fun getEntry(value: T): RegistryEntry<T> {
+ return registryWrapper.getOptional(inverseLookup[value]!!).get()
+ }
+
+ override fun getEntry(key: RegistryKey<T>?): Optional<RegistryEntry.Reference<T>> {
+ return registryWrapper.getOptional(key ?: return Optional.empty())
+ }
+
+ override fun createEntry(value: T): RegistryEntry.Reference<T> {
+ TODO()
+ }
+
+ override fun contains(key: RegistryKey<T>?): Boolean {
+ return getEntry(key).isPresent
+ }
+
+ override fun getEntryLifecycle(entry: T): Lifecycle {
+ return Lifecycle.stable()
+ }
+
+ override fun getId(value: T): Identifier? {
+ return (inverseLookup[value] ?: return null).value
+ }
+
+ override fun getKey(entry: T): Optional<RegistryKey<T>> {
+ return Optional.ofNullable(inverseLookup[entry ?: return Optional.empty()])
+ }
+ }
+}
+
+fun createDynamicRegistry(): DynamicRegistryManager.Immutable {
+ val wrapperLookup = BuiltinRegistries.createWrapperLookup()
+ return object : DynamicRegistryManager.Immutable {
+ override fun <E : Any?> getOptional(key: RegistryKey<out Registry<out E>>): Optional<Registry<E>> {
+ val lookup = wrapperLookup.getOptionalWrapper(key).getOrNull() ?: return Optional.empty()
+ val registry = makeRegistry(lookup, key as RegistryKey<out Registry<E>>)
+ return Optional.of(registry)
+ }
+
+ fun <T> entry(reg: RegistryKey<out Registry<T>>): DynamicRegistryManager.Entry<T> {
+ return DynamicRegistryManager.Entry(reg, getOptional(reg).get())
+ }
+
+ override fun streamAllRegistries(): Stream<DynamicRegistryManager.Entry<*>> {
+ return wrapperLookup.streamAllRegistryKeys()
+ .map { entry(it as RegistryKey<out Registry<Any>>) }
+ }
+ }
+}
+
+class FakeWorld(registries: DynamicRegistryManager.Immutable = createDynamicRegistry()) : World(
+ Properties,
+ RegistryKey.of(RegistryKeys.WORLD, Identifier.of("firmament", "fakeworld")),
+ registries,
+ registries[RegistryKeys.DIMENSION_TYPE].entryOf(
+ RegistryKey.of(
+ RegistryKeys.DIMENSION_TYPE,
+ Identifier("minecraft", "overworld")
+ )
+ ),
+ { DummyProfiler.INSTANCE },
+ true,
+ false,
+ 0, 0
+) {
+ object Properties : MutableWorldProperties {
+ override fun getSpawnX(): Int {
+ return 0
+ }
+
+ override fun getSpawnY(): Int {
+ return 0
+ }
+
+ override fun getSpawnZ(): Int {
+ return 0
+ }
+
+ override fun getSpawnAngle(): Float {
+ return 0F
+ }
+
+ override fun getTime(): Long {
+ return 0
+ }
+
+ override fun getTimeOfDay(): Long {
+ return 0
+ }
+
+ override fun isThundering(): Boolean {
+ return false
+ }
+
+ override fun isRaining(): Boolean {
+ return false
+ }
+
+ override fun setRaining(raining: Boolean) {
+ }
+
+ override fun isHardcore(): Boolean {
+ return false
+ }
+
+ override fun getGameRules(): GameRules {
+ return GameRules()
+ }
+
+ override fun getDifficulty(): Difficulty {
+ return Difficulty.HARD
+ }
+
+ override fun isDifficultyLocked(): Boolean {
+ return false
+ }
+
+ override fun setSpawnX(spawnX: Int) {
+ }
+
+ override fun setSpawnY(spawnY: Int) {
+ }
+
+ override fun setSpawnZ(spawnZ: Int) {
+ }
+
+ override fun setSpawnAngle(spawnAngle: Float) {
+ }
+ }
+
+ override fun getPlayers(): List<PlayerEntity> {
+ return emptyList()
+ }
+
+ override fun getBrightness(direction: Direction?, shaded: Boolean): Float {
+ return 1f
+ }
+
+ override fun getGeneratorStoredBiome(biomeX: Int, biomeY: Int, biomeZ: Int): RegistryEntry<Biome> {
+ return registryManager.get(RegistryKeys.BIOME).entryOf(BiomeKeys.PLAINS)
+ }
+
+ override fun getEnabledFeatures(): FeatureSet {
+ return FeatureFlags.VANILLA_FEATURES
+ }
+
+ class FakeTickScheduler<T> : QueryableTickScheduler<T> {
+ override fun scheduleTick(orderedTick: OrderedTick<T>?) {
+ }
+
+ override fun isQueued(pos: BlockPos?, type: T): Boolean {
+ return true
+ }
+
+ override fun getTickCount(): Int {
+ return 0
+ }
+
+ override fun isTicking(pos: BlockPos?, type: T): Boolean {
+ return true
+ }
+
+ }
+
+ override fun getBlockTickScheduler(): QueryableTickScheduler<Block> {
+ return FakeTickScheduler()
+ }
+
+ override fun getFluidTickScheduler(): QueryableTickScheduler<Fluid> {
+ return FakeTickScheduler()
+ }
+
+
+ class FakeChunkManager(val world: FakeWorld) : ChunkManager() {
+ override fun getChunk(x: Int, z: Int, leastStatus: ChunkStatus?, create: Boolean): Chunk {
+ return EmptyChunk(world, ChunkPos(x,z), world.registryManager.get(RegistryKeys.BIOME).entryOf(BiomeKeys.PLAINS))
+ }
+
+ override fun getWorld(): BlockView {
+ return world
+ }
+
+ override fun tick(shouldKeepTicking: BooleanSupplier?, tickChunks: Boolean) {
+ }
+
+ override fun getDebugString(): String {
+ return "FakeChunkManager"
+ }
+
+ override fun getLoadedChunkCount(): Int {
+ return 0
+ }
+
+ override fun getLightingProvider(): LightingProvider {
+ return FakeLightingProvider(this)
+ }
+ }
+
+ class FakeLightingProvider(chunkManager: FakeChunkManager) : LightingProvider(chunkManager, false, false)
+
+ override fun getChunkManager(): ChunkManager {
+ return FakeChunkManager(this)
+ }
+
+ override fun playSound(
+ source: PlayerEntity?,
+ x: Double,
+ y: Double,
+ z: Double,
+ sound: RegistryEntry<SoundEvent>?,
+ category: SoundCategory?,
+ volume: Float,
+ pitch: Float,
+ seed: Long
+ ) {
+ }
+
+ override fun syncWorldEvent(player: PlayerEntity?, eventId: Int, pos: BlockPos?, data: Int) {
+ }
+
+ override fun emitGameEvent(event: GameEvent?, emitterPos: Vec3d?, emitter: GameEvent.Emitter?) {
+ }
+
+ override fun updateListeners(pos: BlockPos?, oldState: BlockState?, newState: BlockState?, flags: Int) {
+ }
+
+ override fun playSoundFromEntity(
+ source: PlayerEntity?,
+ entity: Entity?,
+ sound: RegistryEntry<SoundEvent>?,
+ category: SoundCategory?,
+ volume: Float,
+ pitch: Float,
+ seed: Long
+ ) {
+ }
+
+ override fun asString(): String {
+ return "FakeWorld"
+ }
+
+ override fun getEntityById(id: Int): Entity? {
+ return null
+ }
+
+ override fun getTickManager(): TickManager {
+ return TickManager()
+ }
+
+ override fun getMapState(id: String?): MapState? {
+ return null
+ }
+
+ override fun putMapState(id: String?, state: MapState?) {
+ }
+
+ override fun getNextMapId(): Int {
+ return 0
+ }
+
+ override fun setBlockBreakingInfo(entityId: Int, pos: BlockPos?, progress: Int) {
+ }
+
+ override fun getScoreboard(): Scoreboard {
+ return Scoreboard()
+ }
+
+ override fun getRecipeManager(): RecipeManager {
+ return RecipeManager()
+ }
+
+ object FakeEntityLookup : EntityLookup<Entity> {
+ override fun get(id: Int): Entity? {
+ return null
+ }
+
+ override fun get(uuid: UUID?): Entity? {
+ return null
+ }
+
+ override fun iterate(): MutableIterable<Entity> {
+ return mutableListOf()
+ }
+
+ override fun <U : Entity?> forEachIntersects(
+ filter: TypeFilter<Entity, U>?,
+ box: Box?,
+ consumer: LazyIterationConsumer<U>?
+ ) {
+ }
+
+ override fun forEachIntersects(box: Box?, action: Consumer<Entity>?) {
+ }
+
+ override fun <U : Entity?> forEach(filter: TypeFilter<Entity, U>?, consumer: LazyIterationConsumer<U>?) {
+ }
+
+ }
+
+ override fun getEntityLookup(): EntityLookup<Entity> {
+ return FakeEntityLookup
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/GuiPlayer.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/GuiPlayer.kt
new file mode 100644
index 0000000..5f88098
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/GuiPlayer.kt
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.mojang.authlib.GameProfile
+import java.util.*
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.client.util.DefaultSkinHelper
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.client.util.SkinTextures.Model
+import net.minecraft.client.world.ClientWorld
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import net.minecraft.world.World
+
+/**
+ * @see moe.nea.firmament.init.EarlyRiser
+ */
+fun makeGuiPlayer(world: FakeWorld): GuiPlayer {
+ val constructor = GuiPlayer::class.java.getDeclaredConstructor(
+ World::class.java,
+ BlockPos::class.java,
+ Float::class.javaPrimitiveType,
+ GameProfile::class.java
+ )
+ return constructor.newInstance(world, BlockPos.ORIGIN, 0F, GameProfile(UUID.randomUUID(), "Linnea"))
+}
+
+class GuiPlayer(world: ClientWorld?, profile: GameProfile?) : AbstractClientPlayerEntity(world, profile) {
+ override fun isSpectator(): Boolean {
+ return false
+ }
+
+ override fun isCreative(): Boolean {
+ return false
+ }
+
+ var skinTexture: Identifier = DefaultSkinHelper.getSkinTextures(this.getUuid()).texture
+ var capeTexture: Identifier? = null
+ var model: Model = Model.WIDE
+ override fun getSkinTextures(): SkinTextures {
+ return SkinTextures(
+ skinTexture,
+ null,
+ capeTexture,
+ null,
+ model,
+ true
+ )
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyAge.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyAge.kt
new file mode 100644
index 0000000..7b80e88
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyAge.kt
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import net.minecraft.entity.mob.ZombieEntity
+import net.minecraft.entity.passive.PassiveEntity
+
+object ModifyAge : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ val isBaby = info["baby"]?.asBoolean ?: false
+ if (entity is PassiveEntity) {
+ entity.breedingAge = if (isBaby) -1 else 1
+ } else if (entity is ZombieEntity) {
+ entity.isBaby = isBaby
+ } else if (entity is ArmorStandEntity) {
+ entity.isSmall = isBaby
+ } else {
+ error("Cannot set age for $entity")
+ }
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyCharged.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyCharged.kt
new file mode 100644
index 0000000..225a8ca
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyCharged.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.mob.CreeperEntity
+
+object ModifyCharged : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is CreeperEntity)
+ entity.dataTracker.set(CreeperEntity.CHARGED, true)
+ return entity
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyEquipment.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyEquipment.kt
new file mode 100644
index 0000000..e438f59
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyEquipment.kt
@@ -0,0 +1,59 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.DyeableArmorItem
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import moe.nea.firmament.rei.SBItemStack
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.item.setEncodedSkullOwner
+import moe.nea.firmament.util.item.zeroUUID
+
+object ModifyEquipment : EntityModifier {
+ val names = mapOf(
+ "hand" to EquipmentSlot.MAINHAND,
+ "helmet" to EquipmentSlot.HEAD,
+ "chestplate" to EquipmentSlot.CHEST,
+ "leggings" to EquipmentSlot.LEGS,
+ "feet" to EquipmentSlot.FEET,
+ )
+
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ names.forEach { (key, slot) ->
+ info[key]?.let {
+ entity.equipStack(slot, createItem(it.asString))
+ }
+ }
+ return entity
+ }
+
+ private fun createItem(item: String): ItemStack {
+ val split = item.split("#")
+ if (split.size != 2) return SBItemStack(SkyblockId(item)).asImmutableItemStack()
+ val (type, data) = split
+ return when (type) {
+ "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(zeroUUID, data) }
+ "LEATHER_LEGGINGS" -> coloredLeatherArmor(Items.LEATHER_LEGGINGS, data)
+ "LEATHER_BOOTS" -> coloredLeatherArmor(Items.LEATHER_BOOTS, data)
+ "LEATHER_HELMET" -> coloredLeatherArmor(Items.LEATHER_HELMET, data)
+ "LEATHER_CHESTPLATE" -> coloredLeatherArmor(Items.LEATHER_CHESTPLATE, data)
+ else -> error("Unknown leather piece: $type")
+ }
+ }
+
+ private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack {
+ require(leatherArmor is DyeableArmorItem)
+ val stack = ItemStack(leatherArmor)
+ leatherArmor.setColor(stack, data.toInt(16))
+ return stack
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyHorse.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyHorse.kt
new file mode 100644
index 0000000..4c49510
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyHorse.kt
@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonNull
+import com.google.gson.JsonObject
+import kotlin.experimental.and
+import kotlin.experimental.inv
+import kotlin.experimental.or
+import net.minecraft.entity.EntityType
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.passive.AbstractHorseEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import moe.nea.firmament.gui.entity.EntityRenderer.fakeWorld
+
+object ModifyHorse : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is AbstractHorseEntity)
+ var entity: AbstractHorseEntity = entity
+ info["kind"]?.let {
+ entity = when (it.asString) {
+ "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld)!!
+ "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld)!!
+ "mule" -> EntityType.MULE.create(fakeWorld)!!
+ "donkey" -> EntityType.DONKEY.create(fakeWorld)!!
+ "horse" -> EntityType.HORSE.create(fakeWorld)!!
+ else -> error("Unknown horse kind $it")
+ }
+ }
+ info["armor"]?.let {
+ if (it is JsonNull) {
+ entity.setHorseArmor(ItemStack.EMPTY)
+ } else {
+ when (it.asString) {
+ "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR))
+ "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR))
+ "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR))
+ else -> error("Unknown horse armor $it")
+ }
+ }
+ }
+ info["saddled"]?.let {
+ entity.setIsSaddled(it.asBoolean)
+ }
+ return entity
+ }
+
+}
+
+fun AbstractHorseEntity.setIsSaddled(shouldBeSaddled: Boolean) {
+ val oldFlag = dataTracker.get(AbstractHorseEntity.HORSE_FLAGS)
+ dataTracker.set(
+ AbstractHorseEntity.HORSE_FLAGS,
+ if (shouldBeSaddled) oldFlag or AbstractHorseEntity.SADDLED_FLAG.toByte()
+ else oldFlag and AbstractHorseEntity.SADDLED_FLAG.toByte().inv()
+ )
+}
+
+fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) {
+ items.setStack(1, itemStack)
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyInvisible.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyInvisible.kt
new file mode 100644
index 0000000..07c6617
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyInvisible.kt
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+
+object ModifyInvisible : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ entity.isInvisible = info.get("invisible")?.asBoolean ?: true
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyName.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyName.kt
new file mode 100644
index 0000000..c74e2e5
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyName.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.text.Text
+
+object ModifyName : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ entity.customName = Text.literal(info.get("name").asString)
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyPlayerSkin.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyPlayerSkin.kt
new file mode 100644
index 0000000..886a17e
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyPlayerSkin.kt
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.entity.LivingEntity
+import net.minecraft.util.Identifier
+
+object ModifyPlayerSkin : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is GuiPlayer)
+ info["cape"]?.let {
+ entity.capeTexture = Identifier(it.asString)
+ }
+ info["skin"]?.let {
+ entity.skinTexture = Identifier(it.asString)
+ }
+ info["slim"]?.let {
+ entity.model = if (it.asBoolean) SkinTextures.Model.SLIM else SkinTextures.Model.WIDE
+ }
+ info["parts"]?.let {
+ // TODO: support parts
+ }
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyRiding.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyRiding.kt
new file mode 100644
index 0000000..b9c462e
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyRiding.kt
@@ -0,0 +1,20 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+
+object ModifyRiding : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ val newEntity = EntityRenderer.constructEntity(info)
+ require(newEntity != null)
+ newEntity.startRiding(entity, true)
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyWither.kt b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyWither.kt
new file mode 100644
index 0000000..a0ddcdd
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/entity/ModifyWither.kt
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.boss.WitherEntity
+
+object ModifyWither : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is WitherEntity)
+ info["tiny"]?.let {
+ entity.setInvulTimer(if (it.asBoolean) 800 else 0)
+ }
+ info["armored"]?.let {
+ entity.health = if (it.asBoolean) 1F else entity.maxHealth
+ }
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt b/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
index d643cbf..7e22a1e 100644
--- a/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
+++ b/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -31,6 +32,7 @@ import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import net.minecraft.util.Identifier
+import moe.nea.firmament.rei.recipes.SBMobDropRecipe
class FirmamentReiPlugin : REIClientPlugin {
@@ -62,6 +64,7 @@ class FirmamentReiPlugin : REIClientPlugin {
override fun registerCategories(registry: CategoryRegistry) {
registry.add(SBCraftingRecipe.Category)
registry.add(SBForgeRecipe.Category)
+ registry.add(SBMobDropRecipe.Category)
}
override fun registerExclusionZones(zones: ExclusionZones) {
@@ -77,6 +80,7 @@ class FirmamentReiPlugin : REIClientPlugin {
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator
)
+ registry.registerDisplayGenerator(SBMobDropRecipe.Category.categoryIdentifier, SkyblockMobDropRecipeDynamicGenerator)
}
override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) {
diff --git a/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt b/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt
index 77e329e..0cdb17e 100644
--- a/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt
+++ b/src/main/kotlin/moe/nea/firmament/rei/SBItemEntryDefinition.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -30,6 +31,7 @@ import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.HypixelPetInfo
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.appendLore
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.skyBlockId
@@ -54,6 +56,7 @@ data class SBItemStack(
val neuItem: NEUItem?,
val stackSize: Int,
val petData: PetData?,
+ val extraLore: List<Text> = emptyList(),
) {
constructor(skyblockId: SkyblockId, petData: PetData) : this(
skyblockId,
@@ -102,12 +105,13 @@ data class SBItemStack(
}
}
- private val itemStack by lazy(LazyThreadSafetyMode.NONE) {
+ private val itemStack: ItemStack by lazy(LazyThreadSafetyMode.NONE) {
if (skyblockId == SkyblockId.COINS)
- return@lazy ItemCache.coinItem(stackSize)
+ return@lazy ItemCache.coinItem(stackSize).also { it.appendLore(extraLore) }
val replacementData = mutableMapOf<String, String>()
injectReplacementDataForPets(replacementData)
return@lazy neuItem.asItemStack(idHint = skyblockId, replacementData).copyWithCount(stackSize)
+ .also { it.appendLore(extraLore) }
}
fun asImmutableItemStack(): ItemStack {
diff --git a/src/main/kotlin/moe/nea/firmament/rei/SkyblockCraftingRecipeDynamicGenerator.kt b/src/main/kotlin/moe/nea/firmament/rei/SkyblockCraftingRecipeDynamicGenerator.kt
index 7bff82b..ac5a1fc 100644
--- a/src/main/kotlin/moe/nea/firmament/rei/SkyblockCraftingRecipeDynamicGenerator.kt
+++ b/src/main/kotlin/moe/nea/firmament/rei/SkyblockCraftingRecipeDynamicGenerator.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -8,6 +9,7 @@ package moe.nea.firmament.rei
import io.github.moulberry.repo.data.NEUCraftingRecipe
import io.github.moulberry.repo.data.NEUForgeRecipe
+import io.github.moulberry.repo.data.NEUMobDropRecipe
import io.github.moulberry.repo.data.NEURecipe
import java.util.*
import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator
@@ -16,6 +18,7 @@ import me.shedaniel.rei.api.common.display.Display
import me.shedaniel.rei.api.common.entry.EntryStack
import moe.nea.firmament.rei.recipes.SBCraftingRecipe
import moe.nea.firmament.rei.recipes.SBForgeRecipe
+import moe.nea.firmament.rei.recipes.SBMobDropRecipe
import moe.nea.firmament.repo.RepoManager
@@ -27,6 +30,10 @@ val SkyblockForgeRecipeDynamicGenerator = neuDisplayGenerator<SBForgeRecipe, NEU
SBForgeRecipe(it)
}
+val SkyblockMobDropRecipeDynamicGenerator = neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> {
+ SBMobDropRecipe(it)
+}
+
inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(noinline mapper: (T) -> D) =
object : DynamicDisplayGenerator<D> {
override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> {
diff --git a/src/main/kotlin/moe/nea/firmament/rei/recipes/SBMobDropRecipe.kt b/src/main/kotlin/moe/nea/firmament/rei/recipes/SBMobDropRecipe.kt
new file mode 100644
index 0000000..5af1f9e
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/rei/recipes/SBMobDropRecipe.kt
@@ -0,0 +1,113 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.rei.recipes
+
+import io.github.moulberry.repo.data.NEUMobDropRecipe
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.Renderer
+import me.shedaniel.rei.api.client.gui.widgets.Widget
+import me.shedaniel.rei.api.client.gui.widgets.Widgets
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory
+import me.shedaniel.rei.api.common.category.CategoryIdentifier
+import me.shedaniel.rei.api.common.util.EntryStacks
+import net.minecraft.block.Blocks
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.gui.entity.EntityRenderer
+import moe.nea.firmament.gui.entity.EntityWidget
+import moe.nea.firmament.rei.SBItemEntryDefinition
+
+class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() {
+ override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier
+
+ object Category : DisplayCategory<SBMobDropRecipe> {
+ override fun getCategoryIdentifier(): CategoryIdentifier<SBMobDropRecipe> =
+ CategoryIdentifier.of(Firmament.MOD_ID, "mob_drop_recipe")
+
+ override fun getTitle(): Text = Text.literal("Mob Drops")
+ override fun getDisplayHeight(): Int {
+ return 100
+ }
+
+ override fun getIcon(): Renderer = EntryStacks.of(Blocks.ANVIL)
+ override fun setupDisplay(display: SBMobDropRecipe, bounds: Rectangle): List<Widget> {
+ return buildList {
+ add(Widgets.createRecipeBase(bounds))
+ val source = display.neuRecipe.render
+ val entity = if (source.startsWith("@")) {
+ EntityRenderer.constructEntity(Identifier(source.substring(1)))
+ } else {
+ EntityRenderer.applyModifiers(source, listOf())
+ }
+ if (entity != null) {
+ val level = display.neuRecipe.level
+ val fullMobName =
+ if (level > 0) Text.translatable("firmament.recipe.mobs.name", level, display.neuRecipe.name)
+ else Text.translatable("firmament.recipe.mobs.name.nolevel", display.neuRecipe.name)
+ val tt = mutableListOf<Text>()
+ tt.add((fullMobName))
+ tt.add(Text.literal(""))
+ if (display.neuRecipe.coins > 0) {
+ tt.add(Text.stringifiedTranslatable("firmament.recipe.mobs.coins", display.neuRecipe.coins))
+ }
+ if (display.neuRecipe.combatExperience > 0) {
+ tt.add(
+ Text.stringifiedTranslatable(
+ "firmament.recipe.mobs.combat",
+ display.neuRecipe.combatExperience
+ )
+ )
+ }
+ if (display.neuRecipe.enchantingExperience > 0) {
+ tt.add(
+ Text.stringifiedTranslatable(
+ "firmament.recipe.mobs.exp",
+ display.neuRecipe.enchantingExperience
+ )
+ )
+ }
+ if (display.neuRecipe.extra != null)
+ display.neuRecipe.extra.mapTo(tt) { Text.literal(it) }
+ if (tt.size == 2)
+ tt.removeAt(1)
+ add(
+ Widgets.withTooltip(
+ EntityWidget(entity, Point(bounds.minX + 5, bounds.minY + 15)),
+ tt
+ )
+ )
+ }
+ add(
+ Widgets.createLabel(Point(bounds.minX + 15, bounds.minY + 5), Text.literal(display.neuRecipe.name))
+ .leftAligned()
+ )
+ var x = bounds.minX + 60
+ var y = bounds.minY + 20
+ for (drop in display.neuRecipe.drops) {
+ val lore = drop.extra.mapTo(mutableListOf()) { Text.literal(it) }
+ if (drop.chance != null) {
+ lore += listOf(Text.translatable("firmament.recipe.mobs.drops", drop.chance))
+ }
+ val item = SBItemEntryDefinition.getEntry(drop.dropItem)
+ .value.copy(extraLore = lore)
+ add(
+ Widgets.createSlot(Point(x, y)).markOutput()
+ .entries(listOf(SBItemEntryDefinition.getEntry(item)))
+ )
+ x += 18
+ if (x > bounds.maxX - 30) {
+ x = bounds.minX + 60
+ y += 18
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/repo/RepoModResourcePack.kt b/src/main/kotlin/moe/nea/firmament/repo/RepoModResourcePack.kt
new file mode 100644
index 0000000..c511c90
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/repo/RepoModResourcePack.kt
@@ -0,0 +1,101 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.repo
+
+import java.io.InputStream
+import java.nio.file.Files
+import java.nio.file.Path
+import net.fabricmc.fabric.api.resource.ModResourcePack
+import net.fabricmc.loader.api.FabricLoader
+import net.fabricmc.loader.api.metadata.ModMetadata
+import kotlin.io.path.exists
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.relativeTo
+import kotlin.streams.asSequence
+import net.minecraft.resource.AbstractFileResourcePack
+import net.minecraft.resource.InputSupplier
+import net.minecraft.resource.ResourcePack
+import net.minecraft.resource.ResourceType
+import net.minecraft.resource.metadata.ResourceMetadataReader
+import net.minecraft.util.Identifier
+import net.minecraft.util.PathUtil
+
+class RepoModResourcePack(val basePath: Path) : ModResourcePack {
+ companion object {
+ fun append(packs: MutableList<in ModResourcePack>) {
+ packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation))
+ }
+ }
+
+ override fun close() {
+ }
+
+ override fun openRoot(vararg segments: String): InputSupplier<InputStream>? {
+ return getFile(segments)?.let { InputSupplier.create(it) }
+ }
+
+ fun getFile(segments: Array<out String>): Path? {
+ PathUtil.validatePath(*segments)
+ val path = segments.fold(basePath, Path::resolve)
+ if (!path.isRegularFile()) return null
+ return path
+ }
+
+ override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? {
+ if (type != ResourceType.CLIENT_RESOURCES) return null
+ if (id.namespace != "neurepo") return null
+ val file = getFile(id.path.split("/").toTypedArray())
+ return file?.let { InputSupplier.create(it) }
+ }
+
+ override fun findResources(
+ type: ResourceType?,
+ namespace: String,
+ prefix: String,
+ consumer: ResourcePack.ResultConsumer
+ ) {
+ if (namespace != "neurepo") return
+ if (type != ResourceType.CLIENT_RESOURCES) return
+
+ val prefixPath = basePath.resolve(prefix)
+ if (!prefixPath.exists())
+ return
+ Files.walk(prefixPath)
+ .asSequence()
+ .map { it.relativeTo(basePath) }
+ .forEach {
+ consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it))
+ }
+ }
+
+ override fun getNamespaces(type: ResourceType?): Set<String> {
+ if (type != ResourceType.CLIENT_RESOURCES) return emptySet()
+ return setOf("neurepo")
+ }
+
+ override fun <T> parseMetadata(metaReader: ResourceMetadataReader<T>): T? {
+ return AbstractFileResourcePack.parseMetadata(
+ metaReader, """
+{
+ "pack": {
+ "pack_format": 12,
+ "description": "NEU Repo Resources"
+ }
+}
+""".trimIndent().byteInputStream()
+ )
+ }
+
+ override fun getName(): String {
+ return "NEU Repo Resources"
+ }
+
+ override fun getFabricModMetadata(): ModMetadata {
+ return FabricLoader.getInstance().getModContainer("firmament")
+ .get().metadata
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/ItemUtil.kt b/src/main/kotlin/moe/nea/firmament/util/ItemUtil.kt
index 4f8c90c..5a7a116 100644
--- a/src/main/kotlin/moe/nea/firmament/util/ItemUtil.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/ItemUtil.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -14,6 +15,7 @@ import net.minecraft.text.Text
fun ItemStack.appendLore(args: List<Text>) {
+ if (args.isEmpty()) return
val compoundTag = getOrCreateSubNbt("display")
val loreList = compoundTag.getOrCreateList("Lore", NbtString.STRING_TYPE)
for (arg in args) {
diff --git a/src/main/kotlin/moe/nea/firmament/util/LoadResource.kt b/src/main/kotlin/moe/nea/firmament/util/LoadResource.kt
new file mode 100644
index 0000000..5a8bfbf
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/LoadResource.kt
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util
+
+import java.io.InputStream
+import kotlin.io.path.inputStream
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.RepoDownloadManager
+
+
+fun Identifier.openFirmamentResource(): InputStream {
+ val resource = MC.resourceManager.getResource(this).getOrNull()
+ if (resource == null) {
+ if (namespace == "neurepo")
+ return RepoDownloadManager.repoSavedLocation.resolve(path).inputStream()
+ error("Could not read resource $this")
+ }
+ return resource.inputStream
+}
+
diff --git a/src/main/kotlin/moe/nea/firmament/util/assertions.kt b/src/main/kotlin/moe/nea/firmament/util/assertions.kt
index 5505422..7f06955 100644
--- a/src/main/kotlin/moe/nea/firmament/util/assertions.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/assertions.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
diff --git a/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt b/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt
index 4b72c5e..a061ee4 100644
--- a/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -18,8 +19,13 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
-import net.minecraft.client.texture.PlayerSkinProvider
+import net.minecraft.block.entity.SkullBlockEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtHelper
import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.set
import moe.nea.firmament.util.assertTrueOr
import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.json.InstantAsLongSerializer
@@ -46,6 +52,38 @@ fun GameProfile.setTextures(textures: MinecraftTexturesPayloadKt) {
}
private val propertyTextures = "textures"
+fun String.padBase64(): String {
+ return this + "=".repeat((4 - (this.length % 4)) % 4)
+}
+
+fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) {
+ assert(this.item == Items.PLAYER_HEAD)
+ val gameProfile = GameProfile(uuid, "LameGuy123")
+ gameProfile.properties.put(propertyTextures, Property(propertyTextures, encodedData.padBase64()))
+ val nbt: NbtCompound = this.orCreateNbt
+ nbt[SkullBlockEntity.SKULL_OWNER_KEY] = NbtHelper.writeGameProfile(
+ NbtCompound(),
+ gameProfile
+ )
+}
+
+val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
+fun ItemStack.setSkullOwner(uuid: UUID, url: String) {
+ assert(this.item == Items.PLAYER_HEAD)
+ val gameProfile = GameProfile(uuid, "LameGuy123")
+ gameProfile.setTextures(
+ MinecraftTexturesPayloadKt(
+ mapOf(MinecraftProfileTexture.Type.SKIN to MinecraftProfileTextureKt(url))
+ )
+ )
+ val nbt: NbtCompound = this.orCreateNbt
+ nbt[SkullBlockEntity.SKULL_OWNER_KEY] = NbtHelper.writeGameProfile(
+ NbtCompound(),
+ gameProfile
+ )
+
+}
+
fun decodeProfileTextureProperty(property: Property): MinecraftTexturesPayloadKt? {
assertTrueOr(property.name == propertyTextures) { return null }
diff --git a/src/main/kotlin/moe/nea/firmament/util/render/TranslatedScissors.kt b/src/main/kotlin/moe/nea/firmament/util/render/TranslatedScissors.kt
new file mode 100644
index 0000000..8f80f1b
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/render/TranslatedScissors.kt
@@ -0,0 +1,27 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util.render
+
+import org.joml.Vector4f
+import net.minecraft.client.gui.DrawContext
+
+fun DrawContext.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) {
+ val pMat = matrices.peek().positionMatrix
+ val target = Vector4f()
+
+ target.set(x1, y1, 0f, 1f)
+ target.mul(pMat)
+ val scissorX1 = target.x
+ val scissorY1 = target.y
+
+ target.set(x2, y2, 0f, 1f)
+ target.mul(pMat)
+ val scissorX2 = target.x
+ val scissorY2 = target.y
+
+ enableScissor(scissorX1.toInt(), scissorY1.toInt(), scissorX2.toInt(), scissorY2.toInt())
+}