aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.gradle.kts11
-rw-r--r--gradle/libs.versions.toml13
-rw-r--r--src/main/java/moe/nea/firmament/init/EarlyRiser.java78
-rw-r--r--src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java33
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java18
-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
-rw-r--r--src/main/resources/assets/firmament/lang/en_us.json6
-rw-r--r--src/main/resources/fabric.mod.json3
-rw-r--r--src/main/resources/firmament.accesswidener6
33 files changed, 1611 insertions, 16 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index 38c1b9f..c8c2af7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -73,6 +73,15 @@ repositories {
maven("https://maven.notenoughupdates.org/releases")
}
+kotlin {
+ sourceSets.all {
+ languageSettings {
+// languageVersion = "2.0"
+ enableLanguageFeature("BreakContinueInInlineLambdas")
+ }
+ }
+}
+
val shadowMe by configurations.creating {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
@@ -109,8 +118,10 @@ dependencies {
modImplementation(libs.modmenu)
modImplementation(libs.libgui)
modImplementation(libs.moulconfig)
+ modImplementation(libs.manninghamMills)
modCompileOnly(libs.explosiveenhancement)
include(libs.libgui)
+ include(libs.manninghamMills)
include(libs.moulconfig)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 91f6c00..388d5c6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -27,7 +27,8 @@ jarvis = "1.1.1"
nealisp = "1.0.0"
explosiveenhancement = "1.2.2-1.20.x"
moulconfig = "3.0.0-beta.5"
-
+manninghamMills = "2.4.1"
+notenoughanimations = "ZLjUeuU8"
[libraries]
minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
@@ -46,8 +47,9 @@ jarvis_api = { module = "moe.nea.jarvis:jarvis-api", version.ref = "jarvis" }
jarvis_fabric = { module = "moe.nea.jarvis:jarvis-fabric", version.ref = "jarvis" }
nealisp = { module = "moe.nea:nealisp", version.ref = "nealisp" }
explosiveenhancement = { module = "maven.modrinth:explosive-enhancement", version.ref = "explosiveenhancement" }
-
+manninghamMills = { module = "me.shedaniel:mm", version.ref = "manninghamMills" }
# Runtime:
+notenoughanimations = { module = "maven.modrinth:not-enough-animations", version.ref = "notenoughanimations" }
hotswap = { module = "virtual.github.hotswapagent:hotswap-agent", version.ref = "hotswap_agent" }
architectury_fabric = { module = "dev.architectury:architectury-fabric", version.ref = "architectury" }
rei_fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" }
@@ -61,7 +63,12 @@ freecammod = { module = "maven.modrinth:freecam", version.ref = "freecammod" }
[bundles]
dbus = ["dbus_java_core", "dbus_java_unixsocket"]
-runtime_required = ["architectury_fabric", "rei_fabric"]
+runtime_required = [
+ "architectury_fabric",
+ "rei_fabric",
+ "notenoughanimations",
+
+]
runtime_optional = [
"devauth",
# "freecammod",
diff --git a/src/main/java/moe/nea/firmament/init/EarlyRiser.java b/src/main/java/moe/nea/firmament/init/EarlyRiser.java
new file mode 100644
index 0000000..268e3f3
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/init/EarlyRiser.java
@@ -0,0 +1,78 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.init;
+
+import me.shedaniel.mm.api.ClassTinkerers;
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.MappingResolver;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.*;
+
+import java.lang.reflect.Modifier;
+import java.util.Objects;
+
+public class EarlyRiser implements Runnable {
+ MappingResolver remapper = FabricLoader.getInstance().getMappingResolver();
+ String PlayerEntity = remapper.mapClassName("intermediary", "net.minecraft.class_1657");
+ String World = remapper.mapClassName("intermediary", "net.minecraft.class_1937");
+ String GameProfile = "com.mojang.authlib.GameProfile";
+ String BlockPos = remapper.mapClassName("intermediary", "net.minecraft.class_2338");
+ String AbstractClientPlayerEntity = remapper.mapClassName("intermediary", "net.minecraft.class_742");
+ String GuiPlayer = "moe.nea.firmament.gui.entity.GuiPlayer";
+ // World world, BlockPos pos, float yaw, GameProfile gameProfile
+ Type constructorDescriptor = Type.getMethodType(Type.VOID_TYPE, getTypeForClassName(World), getTypeForClassName(BlockPos), Type.FLOAT_TYPE, getTypeForClassName(GameProfile));
+
+ @Override
+ public void run() {
+ ClassTinkerers.addTransformation(AbstractClientPlayerEntity, it -> mapClassNode(it, getTypeForClassName(PlayerEntity)));
+ ClassTinkerers.addTransformation(GuiPlayer, it -> mapClassNode(it, getTypeForClassName(AbstractClientPlayerEntity)));
+ }
+
+ private void mapClassNode(ClassNode classNode, Type superClass) {
+ for (MethodNode method : classNode.methods) {
+ if (Objects.equals(method.name, "<init>") && Type.getMethodType(method.desc).equals(constructorDescriptor)) {
+ modifyConstructor(method, superClass);
+ return;
+ }
+ }
+ var node = new MethodNode(Opcodes.ASM9, "<init>", constructorDescriptor.getDescriptor(), null, null);
+ classNode.methods.add(node);
+ modifyConstructor(node, superClass);
+ }
+
+ private Type getTypeForClassName(String className) {
+ return Type.getObjectType(className.replace('.', '/'));
+ }
+
+ private void modifyConstructor(MethodNode method, Type superClass) {
+ method.access = (method.access | Modifier.PUBLIC) & ~Modifier.PRIVATE & ~Modifier.PROTECTED;
+ if (method.instructions.size() != 0) return; // Some other mod has already made a constructor here
+
+ // World world, BlockPos pos, float yaw, GameProfile gameProfile
+ // ALOAD this
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
+
+ // ALOAD World
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1));
+
+ // ALOAD BlockPos
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 2));
+
+ // ALOAD yaw
+ method.instructions.add(new VarInsnNode(Opcodes.FLOAD, 3));
+
+ // ALOAD gameProfile
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 4));
+
+ // Call super
+ method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, superClass.getInternalName(), "<init>", constructorDescriptor.getDescriptor(), false));
+
+ // Return
+ method.instructions.add(new InsnNode(Opcodes.RETURN));
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java
new file mode 100644
index 0000000..729b9b8
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.repo.RepoModResourcePack;
+import net.fabricmc.fabric.api.resource.ModResourcePack;
+import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil;
+import net.minecraft.resource.ResourceType;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.List;
+
+@Mixin(ModResourcePackUtil.class)
+public class AppendRepoAsResourcePack {
+ @Inject(method = "appendModResourcePacks", at = @At("TAIL"))
+ private static void onAppendModResourcePack(
+ List<ModResourcePack> packs,
+ ResourceType type,
+ @Nullable String subPath,
+ CallbackInfo ci
+ ) {
+ RepoModResourcePack.Companion.append(packs);
+ }
+
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java
new file mode 100644
index 0000000..efa4a56
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java
@@ -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.mixins.accessor;
+
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.client.network.PlayerListEntry;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(AbstractClientPlayerEntity.class)
+public interface AccessorAbstractClientPlayerEntity {
+ @Accessor("playerListEntry")
+ void setPlayerListEntry_firmament(PlayerListEntry playerListEntry);
+}
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
+ }
+