From 57052c2eabc54633b68a758eda157dd1fddfd7cf Mon Sep 17 00:00:00 2001 From: Fff Date: Sat, 14 Nov 2026 10:26:54 -0800 Subject: [PATCH] =?UTF-8?q?feat(rendering):=20=E6=8F=90=E4=BE=9B=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E7=BB=93=E6=9E=84=E6=B8=B2=E6=9F=93=E4=B8=8E=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E7=95=8C=E9=9D=A2=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移隔离结构沙盒、相机拾取与原生界面浮窗 - 提供自定义界面注册和附加渲染接口,解除对手册的依赖 - 适配当前游戏版本的渲染与透明度补丁 - 补充通用翻译、接口说明及只读作用域回归检查 --- module.rendering/README.md | 15 + module.rendering/build.gradle | 29 + .../preview/StructureGuiGraphicsMixin.java | 16 + .../preview/StructureGuiRecipeBookMixin.java | 19 + .../preview/StructureGuiRenderStateMixin.java | 21 + .../preview/StructureGuiScreenMixin.java | 20 + .../mixins/preview/package-info.java | 2 + .../preview/SectionOffsetVertexConsumer.java | 136 +++ .../rendering/preview/SodiumSpriteBridge.java | 18 + .../rendering/preview/ViewportCameraRig.java | 141 +++ .../preview/gui/StructureGuiGeometry.java | 40 + .../preview/gui/StructureGuiPreview.java | 230 +++++ .../preview/gui/StructureGuiProviders.java | 47 + .../preview/gui/StructureGuiSession.java | 108 +++ .../rendering/preview/gui/package-info.java | 2 + .../level/DelegatingServerLevelAccessor.java | 864 ++++++++++++++++++ .../preview/level/PreviewLightmapAtlas.java | 110 +++ .../preview/level/SandboxChunkProvider.java | 70 ++ .../preview/level/SandboxLevelChunk.java | 41 + .../preview/level/SandboxRenderLevel.java | 484 ++++++++++ .../level/StructureBlockEntityDetails.java | 144 +++ .../preview/level/StructureDetailsPager.java | 74 ++ .../preview/level/StructurePreviewPicker.java | 77 ++ .../level/StructurePreviewRenderer.java | 441 +++++++++ .../level/StructureSandboxFactory.java | 109 +++ .../rendering/preview/level/package-info.java | 2 + .../v2/rendering/preview/package-info.java | 2 + .../resources/META-INF/neoforge.mods.toml | 3 + .../resources/anvillib_rendering.mixins.json | 15 + .../assets/anvillib_rendering/lang/en_us.json | 109 +++ .../assets/anvillib_rendering/lang/zh_cn.json | 109 +++ .../preview/gui/PreviewRegressionTest.java | 55 ++ 32 files changed, 3553 insertions(+) create mode 100644 module.rendering/README.md create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiGraphicsMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRecipeBookMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRenderStateMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiScreenMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/package-info.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SectionOffsetVertexConsumer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SodiumSpriteBridge.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/ViewportCameraRig.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiGeometry.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiPreview.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiProviders.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiSession.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/package-info.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/DelegatingServerLevelAccessor.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/PreviewLightmapAtlas.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxChunkProvider.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxLevelChunk.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxRenderLevel.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureBlockEntityDetails.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureDetailsPager.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewPicker.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewRenderer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureSandboxFactory.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/package-info.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/package-info.java create mode 100644 module.rendering/src/main/resources/anvillib_rendering.mixins.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/lang/en_us.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/lang/zh_cn.json create mode 100644 module.rendering/src/previewTest/java/dev/anvilcraft/lib/v2/rendering/preview/gui/PreviewRegressionTest.java diff --git a/module.rendering/README.md b/module.rendering/README.md new file mode 100644 index 000000000..93fbf7f14 --- /dev/null +++ b/module.rendering/README.md @@ -0,0 +1,15 @@ +# 共享结构预览 + +`module.rendering` 提供独立于手册和具体机器的结构渲染与只读界面预览。 + +- 使用 `StructureSandboxFactory.create` 将原版 `StructureTemplate` 放入隔离客户端关卡,保存原始方块实体数据,并加载结构中的实体。 +- 使用 `ViewportCameraRig`、`StructurePreviewRenderer` 和 `StructurePreviewPicker` 保持渲染、射线选块及屏幕坐标投影一致。 +- 使用 `StructureGuiPreview` 打开原生容器浮窗。调用方转发鼠标点击、拖动和释放事件,并在每帧更新方块锚点;界面移除或结构改变时调用 `clear`。 +- 使用 `StructureGuiProviders.register` 为特殊存储或无标准菜单的方块注册只读屏幕。提供器只可使用传入的隔离玩家、关卡与结构快照,禁止发送交互请求、修改真实库存或安装当前屏幕。 +- 只读会话不调用交互、tick 或关闭生命周期,初始化与绘制处于 `StructureGuiSession.renderOnly` 作用域,真实玩家菜单在异常时同样恢复。 +- 浮窗构造器可接受 `BooleanSupplier` 提供减少动态效果的偏好;砧库不读取调用方配置。 +- 渲染器的 `Effects` 接口允许提交附加模型与覆盖效果。1.21.1、1.21.4 在相机矩阵有效时执行回调;1.21.8、1.21.11、26.1 在 PiP 纹理目标中执行;26.2、26.3 的覆盖回调收到已完成的纹理和 GUI 状态,应显式使用该纹理并在后续 GUI 层提交覆盖内容,禁止关闭原生持有的纹理。 + +蓝图编辑、扫描范围、扫描特效和模组库存快照由调用方负责。此模块不依赖藿香或铁砧工艺。 + +`previewTest` 验证提供器选择、重复注册保护、嵌套绘制及异常后的作用域恢复,并由 `check` 执行。渲染画面和第三方容器仍需客户端验证。 diff --git a/module.rendering/build.gradle b/module.rendering/build.gradle index c2e92447e..5354dc596 100644 --- a/module.rendering/build.gradle +++ b/module.rendering/build.gradle @@ -286,3 +286,32 @@ if (providers.gradleProperty('cubeLocalTests').isPresent() || providers.gradlePr } apply from: localTests } + +repositories { + exclusiveContent { + forRepository { maven { url = 'https://api.modrinth.com/maven' } } + filter { includeGroup 'maven.modrinth' } + } +} + +dependencies { + compileOnly 'maven.modrinth:sodium:mc1.21.1-0.6.13-neoforge' +} + +sourceSets { + previewTest { + compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + } +} + +tasks.register('previewTest', JavaExec) { + group = 'verification' + description = '验证共享结构预览的注册与只读绘制状态' + dependsOn tasks.named('previewTestClasses') + classpath = sourceSets.previewTest.runtimeClasspath + mainClass = 'dev.anvilcraft.lib.v2.rendering.preview.gui.PreviewRegressionTest' + javaLauncher = javaToolchains.launcherFor { languageVersion = java.toolchain.languageVersion } +} + +tasks.named('check') { dependsOn tasks.named('previewTest') } diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiGraphicsMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiGraphicsMixin.java new file mode 100644 index 000000000..5f7fc32f0 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiGraphicsMixin.java @@ -0,0 +1,16 @@ +package dev.anvilcraft.lib.v2.rendering.mixins.preview; + +import dev.anvilcraft.lib.v2.rendering.preview.gui.StructureGuiSession; +import net.minecraft.client.gui.GuiGraphics; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +/** 原生控件恢复绘制颜色时仍保留整个浮窗的透明度。 */ +@Mixin(GuiGraphics.class) +public abstract class StructureGuiGraphicsMixin { + @ModifyVariable(method = "setColor", at = @At("HEAD"), argsOnly = true, ordinal = 3) + private float anvillib$previewOpacity(float alpha) { + return alpha * StructureGuiSession.opacity(); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRecipeBookMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRecipeBookMixin.java new file mode 100644 index 000000000..6bde8622e --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRecipeBookMixin.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.rendering.mixins.preview; + +import dev.anvilcraft.lib.v2.rendering.preview.gui.StructureGuiSession; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +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.CallbackInfoReturnable; + +/** 预览只展示容器本身,避免沿用玩家的配方书开关并更新其配方已读状态。 */ +@Mixin(RecipeBookComponent.class) +public abstract class StructureGuiRecipeBookMixin { + @Inject(method = "isVisibleAccordingToBookData", at = @At("HEAD"), cancellable = true) + private void anvillib$hidePreviewRecipeBook(CallbackInfoReturnable cir) { + if (StructureGuiSession.isRendering()) { + cir.setReturnValue(false); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRenderStateMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRenderStateMixin.java new file mode 100644 index 000000000..221ae74f0 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiRenderStateMixin.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.rendering.mixins.preview; + +import com.mojang.blaze3d.systems.RenderSystem; +import dev.anvilcraft.lib.v2.rendering.preview.gui.StructureGuiSession; +import net.minecraft.client.renderer.RenderStateShard; +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; + +/** 不透明物品材质在浮窗淡入淡出期间也参与透明混合。 */ +@Mixin(RenderStateShard.class) +public abstract class StructureGuiRenderStateMixin { + @Inject(method = "setupRenderState", at = @At("TAIL")) + private void anvillib$blendPreview(CallbackInfo ci) { + if (StructureGuiSession.isRendering() && StructureGuiSession.opacity() < 1) { + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiScreenMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiScreenMixin.java new file mode 100644 index 000000000..27b4fcf6d --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/StructureGuiScreenMixin.java @@ -0,0 +1,20 @@ +package dev.anvilcraft.lib.v2.rendering.mixins.preview; + +import dev.anvilcraft.lib.v2.rendering.preview.gui.StructureGuiSession; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +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; + +/** 原生 GUI 只在浮窗内绘制自身背景,不再次模糊或遮罩整个手册。 */ +@Mixin(Screen.class) +public abstract class StructureGuiScreenMixin { + @Inject(method = "renderTransparentBackground", at = @At("HEAD"), cancellable = true) + private void anvillib$previewBackground(GuiGraphics graphics, CallbackInfo ci) { + if (StructureGuiSession.isRendering()) { + ci.cancel(); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/package-info.java new file mode 100644 index 000000000..37d5f15ab --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/preview/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package dev.anvilcraft.lib.v2.rendering.mixins.preview; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SectionOffsetVertexConsumer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SectionOffsetVertexConsumer.java new file mode 100644 index 000000000..ae1c5ab92 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SectionOffsetVertexConsumer.java @@ -0,0 +1,136 @@ +package dev.anvilcraft.lib.v2.rendering.preview; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.core.SectionPos; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +/** + * 将区块分段局部坐标转换为世界坐标的 VertexConsumer 包装器。 + */ +public class SectionOffsetVertexConsumer implements VertexConsumer { + private final VertexConsumer delegate; + private final SectionPos sectionPos; + + public SectionOffsetVertexConsumer(VertexConsumer delegate, SectionPos sectionPos) { + this.delegate = delegate; + this.sectionPos = sectionPos; + } + + @Override + public VertexConsumer addVertex(float x, float y, float z) { + // 流体网格输出的是分段原点相对坐标,这里平移到世界坐标系。 + x += sectionPos.getX() * SectionPos.SECTION_SIZE; + y += sectionPos.getY() * SectionPos.SECTION_SIZE; + z += sectionPos.getZ() * SectionPos.SECTION_SIZE; + + return delegate.addVertex(x, y, z); + } + + @Override + public VertexConsumer setColor(int i, int i1, int i2, int i3) { + return delegate.setColor(i, i1, i2, i3); + } + + @Override + public VertexConsumer setUv(float v, float v1) { + return delegate.setUv(v, v1); + } + + @Override + public VertexConsumer setUv1(int i, int i1) { + return delegate.setUv1(i, i1); + } + + @Override + public VertexConsumer setUv2(int i, int i1) { + return delegate.setUv2(i, i1); + } + + @Override + public VertexConsumer setNormal(float v, float v1, float v2) { + return delegate.setNormal(v, v1, v2); + } + + @Override + public void addVertex( + float p_351049_, float p_350528_, float p_351018_, int p_350427_, float p_350508_, + float p_350864_, int p_350846_, int p_350731_, float p_350784_, float p_351051_, float p_350759_ + ) { + delegate.addVertex( + p_351049_, p_350528_, p_351018_, p_350427_, p_350508_, p_350864_, p_350846_, p_350731_, + p_350784_, p_351051_, p_350759_ + ); + } + + @Override + public VertexConsumer setColor(float p_350350_, float p_350356_, float p_350623_, float p_350312_) { + return delegate.setColor(p_350350_, p_350356_, p_350623_, p_350312_); + } + + @Override + public VertexConsumer setColor(int p_350809_) { + return delegate.setColor(p_350809_); + } + + @Override + public VertexConsumer setWhiteAlpha(int p_350979_) { + return delegate.setWhiteAlpha(p_350979_); + } + + @Override + public VertexConsumer setLight(int p_350855_) { + return delegate.setLight(p_350855_); + } + + @Override + public VertexConsumer setOverlay(int p_350697_) { + return delegate.setOverlay(p_350697_); + } + + @Override + public void putBulkData( + PoseStack.Pose pPose, BakedQuad pQuad, float pRed, float pGreen, float pBlue, float pAlpha, + int pPackedLight, int pPackedOverlay + ) { + delegate.putBulkData(pPose, pQuad, pRed, pGreen, pBlue, pAlpha, pPackedLight, pPackedOverlay); + } + + @Override + public void putBulkData( + PoseStack.Pose pPose, BakedQuad pQuad, float[] pBrightness, float pRed, float pGreen, + float pBlue, float pAlpha, int[] pLightmap, int pPackedOverlay, boolean p_331268_ + ) { + delegate.putBulkData( + pPose, pQuad, pBrightness, pRed, pGreen, pBlue, pAlpha, pLightmap, pPackedOverlay, + p_331268_ + ); + } + + @Override + public VertexConsumer addVertex(Vector3f p_350685_) { + return delegate.addVertex(p_350685_); + } + + @Override + public VertexConsumer addVertex(PoseStack.Pose p_352288_, Vector3f p_352298_) { + return delegate.addVertex(p_352288_, p_352298_); + } + + @Override + public VertexConsumer addVertex(PoseStack.Pose p_350506_, float p_350934_, float p_350873_, float p_350981_) { + return delegate.addVertex(p_350506_, p_350934_, p_350873_, p_350981_); + } + + @Override + public VertexConsumer addVertex(Matrix4f p_350929_, float p_350884_, float p_350885_, float p_350942_) { + return delegate.addVertex(p_350929_, p_350884_, p_350885_, p_350942_); + } + + @Override + public VertexConsumer setNormal(PoseStack.Pose p_350592_, float p_350534_, float p_350411_, float p_350441_) { + return delegate.setNormal(p_350592_, p_350534_, p_350411_, p_350441_); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SodiumSpriteBridge.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SodiumSpriteBridge.java new file mode 100644 index 000000000..cc18528dd --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/SodiumSpriteBridge.java @@ -0,0 +1,18 @@ +package dev.anvilcraft.lib.v2.rendering.preview; + +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.neoforged.fml.ModList; +import org.jspecify.annotations.Nullable; + +/** 仅在 Sodium 已安装时通知其更新预览中使用的动画纹理。 */ +public final class SodiumSpriteBridge { + private SodiumSpriteBridge() { + } + + public static void pingSpriteUsage(@Nullable TextureAtlasSprite sprite) { + if (sprite != null && ModList.get().isLoaded("sodium")) { + SpriteUtil.INSTANCE.markSpriteActive(sprite); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/ViewportCameraRig.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/ViewportCameraRig.java new file mode 100644 index 000000000..0d3ad47bf --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/ViewportCameraRig.java @@ -0,0 +1,141 @@ +package dev.anvilcraft.lib.v2.rendering.preview; + +import lombok.Getter; +import lombok.Setter; +import net.minecraft.util.Mth; +import net.minecraft.util.StringRepresentable; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; + +/** + * 结构预览渲染使用的相机参数器。 + * + *

用于构建正交投影与视图矩阵,并尽量贴近原版物品/方块 GUI 的默认朝向。

+ */ +public class ViewportCameraRig { + @Getter + @Setter + private float zoom = 1; + private final Vector4f viewport = new Vector4f(); + private Mode mode = Mode.ORTOGRAPHIC; + @Setter + @Getter + private float rotationX; + @Setter + @Getter + private float rotationY; + @Setter + @Getter + private float rotationZ; + private final Vector3f rotationCenter = new Vector3f(); + @Setter + @Getter + private float offsetX; + @Setter + @Getter + private float offsetY; + + /** + * 根据界面尺寸配置以原点为中心的正交视口。 + */ + public void configureViewport(int width, int height) { + var halfWidth = width / 2f; + var halfHeight = height / 2f; + var renderViewport = new Vector4f( + -halfWidth, + -halfHeight, + halfWidth, + halfHeight + ); + this.viewport.set(renderViewport); + } + + /** + * 创建默认等距视角预设的相机。 + */ + public ViewportCameraRig() { + this.applyPreset(PerspectivePreset.ISOMETRIC_NORTH_WEST); + } + + /** + * 应用一个预定义的预览相机朝向。 + */ + public void applyPreset(PerspectivePreset preset) { + switch (preset) { + case ISOMETRIC_NORTH_EAST -> setOrthographicAngles(225, 30, 0); + case ISOMETRIC_NORTH_WEST -> setOrthographicAngles(135, 30, 0); + case UP -> setOrthographicAngles(120, 0, 45); + } + } + + /** + * 在正交模式下设置偏航/俯仰/滚转角度(单位:度)。 + */ + public void setOrthographicAngles(float yawDeg, float pitchDeg, float rollDeg) { + this.mode = Mode.ORTOGRAPHIC; + rotationY = yawDeg; + rotationX = pitchDeg; + rotationZ = rollDeg; + } + + /** + * 构建供 RenderSystem 使用的视图矩阵。 + */ + public Matrix4f buildViewMatrix() { + var result = new Matrix4f(); + + result.translate(offsetX, offsetY, 0); + + // 0.625f 来源于原版方块模型 JSON 的 GUI 默认变换比例 + result.scale(0.625f * 16 * zoom, 0.625f * 16 * zoom, 0.625f * 16 * zoom); + + if (mode == Mode.ORTOGRAPHIC) { + result.translate(rotationCenter.x, rotationCenter.y, rotationCenter.z); + result.rotateZ(Mth.DEG_TO_RAD * rotationZ); + result.rotateX(Mth.DEG_TO_RAD * rotationX); + result.rotateY(Mth.DEG_TO_RAD * rotationY); + result.translate(-rotationCenter.x, -rotationCenter.y, -rotationCenter.z); + } + + return result; + } + + /** + * 基于当前视口参数构建正交投影矩阵。 + */ + public Matrix4f buildProjectionMatrix() { + var projectionMatrix = new Matrix4f(); + projectionMatrix.setOrtho( + viewport.x(), + viewport.z(), + viewport.y(), + viewport.w(), + -1000, + 3000 + ); + return projectionMatrix; + } + + private enum Mode { + ORTOGRAPHIC, + PERSPECTIVE + } + + public enum PerspectivePreset implements StringRepresentable { + ISOMETRIC_NORTH_EAST("isometric-north-east"), + ISOMETRIC_NORTH_WEST("isometric-north-west"), + UP("up"); + + private final String serializedName; + + PerspectivePreset(String serializedName) { + this.serializedName = serializedName; + } + + @Override + public String getSerializedName() { + return serializedName; + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiGeometry.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiGeometry.java new file mode 100644 index 000000000..0a265f8b5 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiGeometry.java @@ -0,0 +1,40 @@ +package dev.anvilcraft.lib.v2.rendering.preview.gui; + +import org.joml.Vector2f; + +/** 浮窗连线与可中断的短时过渡所用的纯几何运算。 */ +public final class StructureGuiGeometry { + private StructureGuiGeometry() { + } + + /** 从窗口中心朝锚点发出的射线与窗口边缘的交点;锚点在窗口内时不绘制连线。 */ + public static Vector2f edge(float x, float y, float width, float height, float anchorX, float anchorY) { + float cx = x + width / 2, cy = y + height / 2; + float dx = anchorX - cx, dy = anchorY - cy; + if (Math.abs(dx) <= width / 2 && Math.abs(dy) <= height / 2) { + return new Vector2f(anchorX, anchorY); + } + float factor = Math.min(dx == 0 ? Float.POSITIVE_INFINITY : width / 2 / Math.abs(dx), + dy == 0 ? Float.POSITIVE_INFINITY : height / 2 / Math.abs(dy)); + return new Vector2f(cx + dx * factor, cy + dy * factor); + } + + /** cubic-bezier(0.23, 1, 0.32, 1),按横坐标求值。 */ + public static float easeOut(float progress) { + float x = Math.clamp(progress, 0, 1); + float low = 0, high = 1; + for (int i = 0; i < 16; i++) { + float t = (low + high) / 2; + float inv = 1 - t; + float curveX = 3 * inv * inv * t * 0.23f + 3 * inv * t * t * 0.32f + t * t * t; + if (curveX < x) { + low = t; + } + else { + high = t; + } + } + float t = (low + high) / 2; + return 1 - (1 - t) * (1 - t) * (1 - t); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiPreview.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiPreview.java new file mode 100644 index 000000000..ea5228b1c --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiPreview.java @@ -0,0 +1,230 @@ +package dev.anvilcraft.lib.v2.rendering.preview.gui; + +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.joml.Vector2f; +import org.jspecify.annotations.Nullable; + +import java.util.function.BooleanSupplier; + +/** 使用屏幕坐标的只读界面浮窗,调用方提供动画偏好。 */ +@Slf4j +public final class StructureGuiPreview { + private final BooleanSupplier reducedMotion; + + public StructureGuiPreview() { + this(() -> false); + } + + public StructureGuiPreview(BooleanSupplier reducedMotion) { + this.reducedMotion = java.util.Objects.requireNonNull(reducedMotion); + } + + private @Nullable StructureGuiSession session; + private @Nullable Object owner; + private BlockPos block = BlockPos.ZERO; + private float anchorX; + private float anchorY; + private boolean anchorVisible; + private float x; + private float y; + private float scale = 1; + private float progress; + private float transitionStart; + private float target = 1; + private long transitionTime; + private boolean dragging; + private int capturedButton = -1; + private float dragOffsetX; + private float dragOffsetY; + + public boolean open(Object owner, StructureTemplate template, BlockPos pos, float anchorX, float anchorY) { + Minecraft minecraft = Minecraft.getInstance(); + try { + StructureGuiSession next = StructureGuiSession.create(minecraft, template, pos); + if (next == null) { + return false; + } + this.session = next; + this.owner = owner; + this.block = pos.immutable(); + this.anchorX = anchorX; + this.anchorY = anchorY; + this.anchorVisible = true; + this.scale = Math.min(1, Math.min((screenWidth() - 16f) / next.screen().getXSize(), + (screenHeight() - 16f) / next.screen().getYSize())); + this.x = anchorX + 24; + this.y = anchorY - this.height() / 2; + if (this.x + this.width() > screenWidth() - 4) { + this.x = anchorX - 24 - this.width(); + } + this.constrain(); + this.progress = 0; + this.transitionTime = 0; + this.dragging = false; + this.retarget(1); + return true; + } catch (RuntimeException exception) { + log.warn("Cannot preview structure menu at {}", pos, exception); + return false; + } + } + + public @Nullable BlockPos blockFor(Object component) { + return this.owner == component && this.session != null ? this.block : null; + } + + public void beginFrame() { + this.anchorVisible = false; + } + + public void anchor(float x, float y, boolean visible) { + this.anchorX = x; + this.anchorY = y; + this.anchorVisible = visible; + } + + public boolean contains(double mouseX, double mouseY) { + return this.session != null && mouseX >= this.x && mouseX < this.x + this.width() + && mouseY >= this.y && mouseY < this.y + this.height(); + } + + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.contains(mouseX, mouseY)) { + return false; + } + this.capturedButton = button; + if (button == 1) { + this.dragging = false; + this.retarget(0); + } else if (button == 0 && this.target != 0) { + this.dragging = true; + this.dragOffsetX = (float) mouseX - this.x; + this.dragOffsetY = (float) mouseY - this.y; + } + return true; + } + + public boolean mouseDragged(double mouseX, double mouseY, int button) { + if (this.capturedButton != button) { + return false; + } + if (this.dragging) { + this.x = (float) mouseX - this.dragOffsetX; + this.y = (float) mouseY - this.dragOffsetY; + this.constrain(); + } + return true; + } + + public boolean mouseReleased(int button) { + if (this.capturedButton != button) { + return false; + } + this.dragging = false; + this.capturedButton = -1; + return true; + } + + public void clear() { + // 原生 onClose/removed 可能发包或归还槽位物品;预览从不调用这些交互生命周期。 + this.session = null; + this.owner = null; + this.dragging = false; + this.capturedButton = -1; + } + + public void render(GuiGraphics graphics) { + if (this.session == null) { + return; + } + this.updateProgress(); + if (this.target == 0 && this.progress <= 0.001f) { + this.clear(); + return; + } + this.constrain(); + float motion = this.reducedMotion.getAsBoolean() ? 1 : 0.95f + this.progress * 0.05f; + float width = this.width() * motion, height = this.height() * motion; + float x = this.x + (this.width() - width) / 2, y = this.y + (this.height() - height) / 2; + int color = ((int) (255 * this.progress) << 24) | 0xFFFFFF; + graphics.flush(); + if (this.anchorVisible) { + Vector2f edge = new Vector2f(x + width / 2, y + height / 2); + drawLine(graphics, this.anchorX, this.anchorY, edge.x, edge.y, color); + if (edge.distanceSquared(this.anchorX, this.anchorY) > 0.1f) { + // 半径 2 的圆点,比 1 像素连线更醒目。 + for (int row = -2; row <= 2; row++) { + int half = row == -2 || row == 2 ? 1 : 2; + graphics.fill(Math.round(this.anchorX) - half, Math.round(this.anchorY) + row, + Math.round(this.anchorX) + half + 1, Math.round(this.anchorY) + row + 1, color); + } + } + } + graphics.flush(); + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 100); + graphics.pose().scale(this.scale * motion, this.scale * motion, 1); + var screen = this.session.screen(); + graphics.enableScissor((int) Math.floor(x), (int) Math.floor(y), + (int) Math.ceil(x + width), (int) Math.ceil(y + height)); + try { + StructureGuiSession.renderOnly(this.progress, () -> { + graphics.setColor(1, 1, 1, 1); + try { + screen.render(graphics, -100000, -100000, 0); + } finally { + graphics.flush(); + } + }); + } catch (RuntimeException exception) { + log.warn("Cannot render read-only structure menu", exception); + this.clear(); + } finally { + graphics.setColor(1, 1, 1, 1); + graphics.disableScissor(); + graphics.pose().popPose(); + } + } + + private static void drawLine(GuiGraphics graphics, float x0, float y0, float x1, float y1, int color) { + float dx = x1 - x0, dy = y1 - y0; + float length = (float) Math.hypot(dx, dy); + if (length < 0.1f) { + return; + } + graphics.pose().pushPose(); + graphics.pose().translate(x0, y0, 99); + graphics.pose().mulPose(new org.joml.Quaternionf().rotationZ((float) Math.atan2(dy, dx))); + graphics.fill(0, 0, Math.round(length), 1, color); + graphics.pose().popPose(); + } + + private void retarget(float target) { + this.updateProgress(); + this.transitionStart = this.progress; + this.target = target; + this.transitionTime = System.nanoTime(); + } + + private void updateProgress() { + if (this.transitionTime == 0) { + return; + } + float elapsed = (System.nanoTime() - this.transitionTime) / 200_000_000f; + this.progress = this.transitionStart + (this.target - this.transitionStart) * StructureGuiGeometry.easeOut(elapsed); + } + + private void constrain() { + this.x = Math.clamp(this.x, 0, Math.max(0, screenWidth() - this.width())); + this.y = Math.clamp(this.y, 0, Math.max(0, screenHeight() - this.height())); + } + + private float width() { return this.session == null ? 0 : this.session.screen().getXSize() * this.scale; } + private float height() { return this.session == null ? 0 : this.session.screen().getYSize() * this.scale; } + private static int screenWidth() { return Minecraft.getInstance().getWindow().getGuiScaledWidth(); } + private static int screenHeight() { return Minecraft.getInstance().getWindow().getGuiScaledHeight(); } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiProviders.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiProviders.java new file mode 100644 index 000000000..96de50b68 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiProviders.java @@ -0,0 +1,47 @@ +package dev.anvilcraft.lib.v2.rendering.preview.gui; + +import dev.anvilcraft.lib.v2.rendering.preview.level.SandboxRenderLevel; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** 允许模组为不使用原版菜单注册机制的方块提供只读预览界面。 */ +public final class StructureGuiProviders { + private static final Map PROVIDERS = new HashMap<>(); + + private StructureGuiProviders() { + } + + /** 在客户端初始化阶段注册;每种方块只能注册一个提供器。 */ + public static void register(ResourceLocation block, Provider provider) { + Objects.requireNonNull(block); + Objects.requireNonNull(provider); + if (PROVIDERS.putIfAbsent(block, provider) != null) { + throw new IllegalStateException("Structure GUI provider already registered for " + block); + } + } + + static @Nullable Provider get(ResourceLocation block) { + return PROVIDERS.get(block); + } + + /** + * 提供器必须使用上下文中的隔离玩家和沙盒数据,不得安装当前屏幕、发包或访问真实库存。 + * 返回的界面仅接收初始化和绘制,不会接收槽位交互、更新或关闭回调。 + */ + @FunctionalInterface + public interface Provider { + @Nullable AbstractContainerScreen create(Context context); + } + + /** 原始方块实体数据可通过 level.getStructureBlockEntityData(pos) 读取。 */ + public record Context(SandboxRenderLevel level, Player player, BlockPos pos, StructureTemplate template) { + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiSession.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiSession.java new file mode 100644 index 000000000..489422fae --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/StructureGuiSession.java @@ -0,0 +1,108 @@ +package dev.anvilcraft.lib.v2.rendering.preview.gui; + +import dev.anvilcraft.lib.v2.rendering.preview.level.SandboxRenderLevel; +import dev.anvilcraft.lib.v2.rendering.preview.level.StructureSandboxFactory; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.jspecify.annotations.Nullable; + +/** 原生菜单的独立只读会话;不安装为当前屏幕,不执行交互、更新和关闭回调。 */ +public record StructureGuiSession(AbstractContainerScreen screen, SandboxRenderLevel level, Player player) { + private static int renderingDepth; + private static float opacity = 1; + + public static boolean isRendering() { + return renderingDepth > 0; + } + + public static void renderOnly(Runnable render) { + renderOnly(1, render); + } + + public static float opacity() { return isRendering() ? opacity : 1; } + + public static int tint(int color) { + return !isRendering() ? color : ((int) ((color >>> 24) * opacity) << 24) | (color & 0xFFFFFF); + } + + public static void renderOnly(float alpha, Runnable render) { + float previousOpacity = opacity; + opacity = alpha; + renderingDepth++; + try { + render.run(); + } finally { + renderingDepth--; + opacity = previousOpacity; + } + } + + public static @Nullable StructureGuiSession create(Minecraft minecraft, StructureTemplate template, BlockPos pos) { + if (minecraft.player == null) { + return null; + } + SandboxRenderLevel level = StructureSandboxFactory.create(minecraft.level, template, + StructureSandboxFactory.centeredPlacement(template)); + if (level == null) { + return null; + } + level.restoreBlockEntitiesForInspection(); + Player player = new Player(level, pos, 0, minecraft.player.getGameProfile()) { + @Override + public boolean isSpectator() { return false; } + @Override + public boolean isCreative() { return true; } + }; + player.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); + AbstractContainerMenu originalMenu = minecraft.player.containerMenu; + try { + AbstractContainerScreen container; + var custom = StructureGuiProviders.get(BuiltInRegistries.BLOCK.getKey(level.getBlockState(pos).getBlock())); + if (custom != null) { + container = custom.create(new StructureGuiProviders.Context(level, player, pos.immutable(), template)); + if (container == null) { + return null; + } + player.containerMenu = container.getMenu(); + } else { + MenuProvider provider = level.getBlockState(pos).getMenuProvider(level, pos); + if (provider == null && level.getBlockEntity(pos) instanceof MenuProvider entityProvider) { + provider = entityProvider; + } + if (provider == null) { + return null; + } + AbstractContainerMenu menu = provider.createMenu(0, player.getInventory(), player); + if (menu == null) { + return null; + } + player.containerMenu = menu; + var factory = MenuScreens.getScreenFactory((MenuType) menu.getType()); + if (factory.isEmpty()) { + return null; + } + var screen = factory.get().create(menu, player.getInventory(), provider.getDisplayName()); + if (!(screen instanceof AbstractContainerScreen nativeScreen)) { + return null; + } + container = nativeScreen; + } + renderOnly(() -> container.init(minecraft, container.getXSize(), container.getYSize())); + // 部分菜单在 init 中确定最终大小。 + if (container.getGuiLeft() != 0 || container.getGuiTop() != 0) { + renderOnly(() -> container.resize(minecraft, container.getXSize(), container.getYSize())); + } + return new StructureGuiSession(container, level, player); + } finally { + minecraft.player.containerMenu = originalMenu; + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/package-info.java new file mode 100644 index 000000000..12c1ed7c0 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/gui/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package dev.anvilcraft.lib.v2.rendering.preview.gui; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/DelegatingServerLevelAccessor.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/DelegatingServerLevelAccessor.java new file mode 100644 index 000000000..1b74ded29 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/DelegatingServerLevelAccessor.java @@ -0,0 +1,864 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.Difficulty; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.targeting.TargetingConditions; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.ClipBlockStateContext; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.ColorResolver; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.TickPriority; +import net.neoforged.neoforge.client.model.data.ModelData; +import net.neoforged.neoforge.common.world.AuxiliaryLightManager; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * 将调用转发到底层 {@link LevelAccessor} 的 ServerLevelAccessor 适配器。 + * + *

用于结构模板放置阶段必须提供服务端访问器接口的场景, + * 实际执行仍发生在预览客户端关卡中。

+ */ +public class DelegatingServerLevelAccessor implements ServerLevelAccessor { + private final LevelAccessor delegate; + + public DelegatingServerLevelAccessor(LevelAccessor delegate) { + this.delegate = delegate; + } + + @Override + public ServerLevel getLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public long dayTime() { + return this.delegate.dayTime(); + } + + @Override + public long nextSubTickCount() { + return this.delegate.nextSubTickCount(); + } + + @Override + public LevelTickAccess getBlockTicks() { + return this.delegate.getBlockTicks(); + } + + @Override + public void scheduleTick(BlockPos pos, Block block, int delay, TickPriority priority) { + this.delegate.scheduleTick(pos, block, delay, priority); + } + + @Override + public void scheduleTick(BlockPos pos, Block block, int delay) { + this.delegate.scheduleTick(pos, block, delay); + } + + @Override + public LevelTickAccess getFluidTicks() { + return this.delegate.getFluidTicks(); + } + + @Override + public void scheduleTick(BlockPos pos, Fluid fluid, int delay, TickPriority priority) { + this.delegate.scheduleTick(pos, fluid, delay, priority); + } + + @Override + public void scheduleTick(BlockPos pos, Fluid fluid, int delay) { + this.delegate.scheduleTick(pos, fluid, delay); + } + + @Override + public LevelData getLevelData() { + return this.delegate.getLevelData(); + } + + @Override + public DifficultyInstance getCurrentDifficultyAt(BlockPos pos) { + return this.delegate.getCurrentDifficultyAt(pos); + } + + @Override + @Nullable + public MinecraftServer getServer() { + return this.delegate.getServer(); + } + + @Override + public Difficulty getDifficulty() { + return this.delegate.getDifficulty(); + } + + @Override + public ChunkSource getChunkSource() { + return this.delegate.getChunkSource(); + } + + @Override + public boolean hasChunk(int chunkX, int chunkZ) { + return this.delegate.hasChunk(chunkX, chunkZ); + } + + @Override + public RandomSource getRandom() { + return this.delegate.getRandom(); + } + + @Override + public void blockUpdated(BlockPos pos, Block block) { + this.delegate.blockUpdated(pos, block); + } + + @Override + public void neighborShapeChanged( + Direction direction, BlockState queried, BlockPos pos, BlockPos offsetPos, + int flags, int recursionLevel + ) { + this.delegate.neighborShapeChanged(direction, queried, pos, offsetPos, flags, recursionLevel); + } + + @Override + public void playSound(@Nullable Player player, BlockPos pos, SoundEvent sound, SoundSource source) { + this.delegate.playSound(player, pos, sound, source); + } + + @Override + public void playSound( + @Nullable Player player, BlockPos pos, SoundEvent sound, SoundSource source, float volume, + float pitch + ) { + this.delegate.playSound(player, pos, sound, source, volume, pitch); + } + + @Override + public void addParticle( + ParticleOptions particleData, double x, double y, double z, double xSpeed, double ySpeed, + double zSpeed + ) { + this.delegate.addParticle(particleData, x, y, z, xSpeed, ySpeed, zSpeed); + } + + @Override + public void levelEvent(@Nullable Player player, int type, BlockPos pos, int data) { + this.delegate.levelEvent(player, type, pos, data); + } + + @Override + public void levelEvent(int type, BlockPos pos, int data) { + this.delegate.levelEvent(type, pos, data); + } + + @Override + public void gameEvent(Holder event, Vec3 position, GameEvent.Context context) { + this.delegate.gameEvent(event, position, context); + } + + @Override + public void gameEvent(@Nullable Entity entity, Holder event, Vec3 position) { + this.delegate.gameEvent(entity, event, position); + } + + @Override + public void gameEvent(@Nullable Entity entity, Holder event, BlockPos pos) { + this.delegate.gameEvent(entity, event, pos); + } + + @Override + public void gameEvent(Holder event, BlockPos pos, GameEvent.Context context) { + this.delegate.gameEvent(event, pos, context); + } + + @Override + public Optional getBlockEntity(BlockPos pos, BlockEntityType type) { + return this.delegate.getBlockEntity(pos, type); + } + + @Override + public List getEntityCollisions(@Nullable Entity entity, AABB collisionBox) { + return this.delegate.getEntityCollisions(entity, collisionBox); + } + + @Override + public boolean isUnobstructed(@Nullable Entity entity, VoxelShape shape) { + return this.delegate.isUnobstructed(entity, shape); + } + + @Override + public BlockPos getHeightmapPos(Heightmap.Types heightmapType, BlockPos pos) { + return this.delegate.getHeightmapPos(heightmapType, pos); + } + + @Override + public List getEntities(@Nullable Entity entity, AABB area, Predicate predicate) { + return this.delegate.getEntities(entity, area, predicate); + } + + @Override + public List getEntities( + EntityTypeTest entityTypeTest, AABB bounds, + Predicate predicate + ) { + return this.delegate.getEntities(entityTypeTest, bounds, predicate); + } + + @Override + public List getEntitiesOfClass(Class clazz, AABB area, Predicate filter) { + return this.delegate.getEntitiesOfClass(clazz, area, filter); + } + + @Override + public List players() { + return this.delegate.players(); + } + + @Override + public List getEntities(@Nullable Entity entity, AABB area) { + return this.delegate.getEntities(entity, area); + } + + @Override + public List getEntitiesOfClass(Class entityClass, AABB area) { + return this.delegate.getEntitiesOfClass(entityClass, area); + } + + @Override + @Nullable + public Player getNearestPlayer( + double x, double y, double z, double distance, + @Nullable Predicate predicate + ) { + return this.delegate.getNearestPlayer(x, y, z, distance, predicate); + } + + @Override + @Nullable + public Player getNearestPlayer(Entity entity, double distance) { + return this.delegate.getNearestPlayer(entity, distance); + } + + @Override + @Nullable + public Player getNearestPlayer(double x, double y, double z, double distance, boolean creativePlayers) { + return this.delegate.getNearestPlayer(x, y, z, distance, creativePlayers); + } + + @Override + public boolean hasNearbyAlivePlayer(double x, double y, double z, double distance) { + return this.delegate.hasNearbyAlivePlayer(x, y, z, distance); + } + + @Override + @Nullable + public Player getNearestPlayer(TargetingConditions predicate, LivingEntity target) { + return this.delegate.getNearestPlayer(predicate, target); + } + + @Override + @Nullable + public Player getNearestPlayer(TargetingConditions predicate, LivingEntity target, double x, double y, double z) { + return this.delegate.getNearestPlayer(predicate, target, x, y, z); + } + + @Override + @Nullable + public Player getNearestPlayer(TargetingConditions predicate, double x, double y, double z) { + return this.delegate.getNearestPlayer(predicate, x, y, z); + } + + @Override + @Nullable + public T getNearestEntity( + Class entityClazz, TargetingConditions conditions, + @Nullable LivingEntity target, double x, double y, double z, AABB boundingBox + ) { + return this.delegate.getNearestEntity(entityClazz, conditions, target, x, y, z, boundingBox); + } + + @Override + @Nullable + public T getNearestEntity( + List entities, TargetingConditions predicate, + @Nullable LivingEntity target, double x, double y, double z + ) { + return this.delegate.getNearestEntity(entities, predicate, target, x, y, z); + } + + @Override + public List getNearbyPlayers(TargetingConditions predicate, LivingEntity target, AABB area) { + return this.delegate.getNearbyPlayers(predicate, target, area); + } + + @Override + public List getNearbyEntities( + Class entityClazz, TargetingConditions entityPredicate, + LivingEntity entity, AABB area + ) { + return this.delegate.getNearbyEntities(entityClazz, entityPredicate, entity, area); + } + + @Override + @Nullable + public Player getPlayerByUUID(UUID uniqueId) { + return this.delegate.getPlayerByUUID(uniqueId); + } + + @Override + @Nullable + public ChunkAccess getChunk(int x, int z, ChunkStatus requiredStatus, boolean nonnull) { + return this.delegate.getChunk(x, z, requiredStatus, nonnull); + } + + @Override + public int getHeight(Heightmap.Types heightmapType, int x, int z) { + return this.delegate.getHeight(heightmapType, x, z); + } + + @Override + public int getSkyDarken() { + return this.delegate.getSkyDarken(); + } + + @Override + public BiomeManager getBiomeManager() { + return this.delegate.getBiomeManager(); + } + + @Override + public Holder getBiome(BlockPos pos) { + return this.delegate.getBiome(pos); + } + + @Override + public Stream getBlockStatesIfLoaded(AABB aabb) { + return this.delegate.getBlockStatesIfLoaded(aabb); + } + + @Override + public int getBlockTint(BlockPos blockPos, ColorResolver colorResolver) { + return this.delegate.getBlockTint(blockPos, colorResolver); + } + + @Override + public Holder getNoiseBiome(int i, int j, int k) { + return this.delegate.getNoiseBiome(i, j, k); + } + + @Override + public Holder getUncachedNoiseBiome(int x, int y, int z) { + return this.delegate.getUncachedNoiseBiome(x, y, z); + } + + @Override + public boolean isClientSide() { + return this.delegate.isClientSide(); + } + + @Override + public int getSeaLevel() { + return this.delegate.getSeaLevel(); + } + + @Override + public DimensionType dimensionType() { + return this.delegate.dimensionType(); + } + + @Override + public int getMinBuildHeight() { + return this.delegate.getMinBuildHeight(); + } + + @Override + public int getHeight() { + return this.delegate.getHeight(); + } + + @Override + public boolean isEmptyBlock(BlockPos pos) { + return this.delegate.isEmptyBlock(pos); + } + + @Override + public boolean canSeeSkyFromBelowWater(BlockPos pos) { + return this.delegate.canSeeSkyFromBelowWater(pos); + } + + @Override + public float getPathfindingCostFromLightLevels(BlockPos blockPos) { + return this.delegate.getPathfindingCostFromLightLevels(blockPos); + } + + @Override + public float getLightLevelDependentMagicValue(BlockPos blockPos) { + return this.delegate.getLightLevelDependentMagicValue(blockPos); + } + + @Override + public int getDirectSignal(BlockPos pos, Direction direction) { + return this.delegate.getDirectSignal(pos, direction); + } + + @Override + public ChunkAccess getChunk(BlockPos pos) { + return this.delegate.getChunk(pos); + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ) { + return this.delegate.getChunk(chunkX, chunkZ); + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus requiredStatus) { + return this.delegate.getChunk(chunkX, chunkZ, requiredStatus); + } + + @Override + @Nullable + public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) { + return this.delegate.getChunkForCollisions(chunkX, chunkZ); + } + + @Override + public boolean isWaterAt(BlockPos pos) { + return this.delegate.isWaterAt(pos); + } + + @Override + public boolean containsAnyLiquid(AABB bb) { + return this.delegate.containsAnyLiquid(bb); + } + + @Override + public int getMaxLocalRawBrightness(BlockPos pos) { + return this.delegate.getMaxLocalRawBrightness(pos); + } + + @Override + public int getMaxLocalRawBrightness(BlockPos pos, int amount) { + return this.delegate.getMaxLocalRawBrightness(pos, amount); + } + + @Override + public boolean hasChunkAt(int x, int z) { + return this.delegate.hasChunkAt(x, z); + } + + @Override + public boolean hasChunkAt(BlockPos pos) { + return this.delegate.hasChunkAt(pos); + } + + @Override + public boolean hasChunksAt(BlockPos from, BlockPos to) { + return this.delegate.hasChunksAt(from, to); + } + + @Override + public boolean hasChunksAt(int fromX, int fromY, int fromZ, int toX, int toY, int toZ) { + return this.delegate.hasChunksAt(fromX, fromY, fromZ, toX, toY, toZ); + } + + @Override + public boolean hasChunksAt(int fromX, int fromZ, int toX, int toZ) { + return this.delegate.hasChunksAt(fromX, fromZ, toX, toZ); + } + + @Override + public RegistryAccess registryAccess() { + return this.delegate.registryAccess(); + } + + @Override + public FeatureFlagSet enabledFeatures() { + return this.delegate.enabledFeatures(); + } + + @Override + public HolderLookup holderLookup(ResourceKey> resourceKey) { + return this.delegate.holderLookup(resourceKey); + } + + @Override + public float getShade(Direction direction, boolean shade) { + return this.delegate.getShade(direction, shade); + } + + @Override + public LevelLightEngine getLightEngine() { + return this.delegate.getLightEngine(); + } + + @Override + public int getBrightness(LightLayer lightType, BlockPos blockPos) { + return this.delegate.getBrightness(lightType, blockPos); + } + + @Override + public int getRawBrightness(BlockPos blockPos, int amount) { + return this.delegate.getRawBrightness(blockPos, amount); + } + + @Override + public boolean canSeeSky(BlockPos blockPos) { + return this.delegate.canSeeSky(blockPos); + } + + @Override + @Nullable + public BlockEntity getBlockEntity(BlockPos pos) { + return this.delegate.getBlockEntity(pos); + } + + @Override + public BlockState getBlockState(BlockPos pos) { + return this.delegate.getBlockState(pos); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return this.delegate.getFluidState(pos); + } + + @Override + public int getLightEmission(BlockPos pos) { + return this.delegate.getLightEmission(pos); + } + + @Override + public int getMaxLightLevel() { + return this.delegate.getMaxLightLevel(); + } + + @Override + public Stream getBlockStates(AABB area) { + return this.delegate.getBlockStates(area); + } + + @Override + public BlockHitResult isBlockInLine(ClipBlockStateContext context) { + return this.delegate.isBlockInLine(context); + } + + @Override + public BlockHitResult clip(ClipContext context) { + return this.delegate.clip(context); + } + + @Override + @Nullable + public BlockHitResult clipWithInteractionOverride( + Vec3 startVec, Vec3 endVec, BlockPos pos, VoxelShape shape, + BlockState state + ) { + return this.delegate.clipWithInteractionOverride(startVec, endVec, pos, shape, state); + } + + @Override + public double getBlockFloorHeight(VoxelShape shape, Supplier belowShapeSupplier) { + return this.delegate.getBlockFloorHeight(shape, belowShapeSupplier); + } + + @Override + public double getBlockFloorHeight(BlockPos pos) { + return this.delegate.getBlockFloorHeight(pos); + } + + public static T traverseBlocks( + Vec3 from, Vec3 to, C context, BiFunction tester, + Function onFail + ) { + return BlockGetter.traverseBlocks(from, to, context, tester, onFail); + } + + @Override + public int getMaxBuildHeight() { + return this.delegate.getMaxBuildHeight(); + } + + @Override + public int getSectionsCount() { + return this.delegate.getSectionsCount(); + } + + @Override + public int getMinSection() { + return this.delegate.getMinSection(); + } + + @Override + public int getMaxSection() { + return this.delegate.getMaxSection(); + } + + @Override + public boolean isOutsideBuildHeight(BlockPos pos) { + return this.delegate.isOutsideBuildHeight(pos); + } + + @Override + public boolean isOutsideBuildHeight(int y) { + return this.delegate.isOutsideBuildHeight(y); + } + + @Override + public int getSectionIndex(int y) { + return this.delegate.getSectionIndex(y); + } + + @Override + public int getSectionIndexFromSectionY(int sectionIndex) { + return this.delegate.getSectionIndexFromSectionY(sectionIndex); + } + + @Override + public int getSectionYFromSectionIndex(int sectionIndex) { + return this.delegate.getSectionYFromSectionIndex(sectionIndex); + } + + public static LevelHeightAccessor create(int minBuildHeight, int height) { + return LevelHeightAccessor.create(minBuildHeight, height); + } + + @Override + public WorldBorder getWorldBorder() { + return this.delegate.getWorldBorder(); + } + + @Override + public boolean isUnobstructed(BlockState state, BlockPos pos, CollisionContext context) { + return this.delegate.isUnobstructed(state, pos, context); + } + + @Override + public boolean isUnobstructed(Entity entity) { + return this.delegate.isUnobstructed(entity); + } + + @Override + public boolean noCollision(AABB collisionBox) { + return this.delegate.noCollision(collisionBox); + } + + @Override + public boolean noCollision(Entity entity) { + return this.delegate.noCollision(entity); + } + + @Override + public boolean noCollision(@Nullable Entity entity, AABB collisionBox) { + return this.delegate.noCollision(entity, collisionBox); + } + + @Override + public Iterable getCollisions(@Nullable Entity entity, AABB collisionBox) { + return this.delegate.getCollisions(entity, collisionBox); + } + + @Override + public Iterable getBlockCollisions(@Nullable Entity entity, AABB collisionBox) { + return this.delegate.getBlockCollisions(entity, collisionBox); + } + + @Override + public boolean collidesWithSuffocatingBlock(@Nullable Entity entity, AABB box) { + return this.delegate.collidesWithSuffocatingBlock(entity, box); + } + + @Override + public Optional findFreePosition( + @Nullable Entity entity, VoxelShape shape, Vec3 pos, double x, double y, + double z + ) { + return this.delegate.findFreePosition(entity, shape, pos, x, y, z); + } + + @Override + public boolean isStateAtPosition(BlockPos pos, Predicate state) { + return this.delegate.isStateAtPosition(pos, state); + } + + @Override + public boolean isFluidAtPosition(BlockPos pos, Predicate predicate) { + return this.delegate.isFluidAtPosition(pos, predicate); + } + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) { + return this.delegate.setBlock(pos, state, flags, recursionLeft); + } + + @Override + public boolean setBlock(BlockPos pos, BlockState newState, int flags) { + return this.delegate.setBlock(pos, newState, flags); + } + + @Override + public boolean removeBlock(BlockPos pos, boolean isMoving) { + return this.delegate.removeBlock(pos, isMoving); + } + + @Override + public boolean destroyBlock(BlockPos pos, boolean dropBlock) { + return this.delegate.destroyBlock(pos, dropBlock); + } + + @Override + public boolean destroyBlock(BlockPos pos, boolean dropBlock, @Nullable Entity entity) { + return this.delegate.destroyBlock(pos, dropBlock, entity); + } + + @Override + public boolean destroyBlock(BlockPos pos, boolean dropBlock, @Nullable Entity entity, int recursionLeft) { + return this.delegate.destroyBlock(pos, dropBlock, entity, recursionLeft); + } + + @Override + public boolean addFreshEntity(Entity entity) { + return this.delegate.addFreshEntity(entity); + } + + @Override + public float getMoonBrightness() { + return this.delegate.getMoonBrightness(); + } + + @Override + public float getTimeOfDay(float partialTick) { + return this.delegate.getTimeOfDay(partialTick); + } + + @Override + public int getMoonPhase() { + return this.delegate.getMoonPhase(); + } + + @Override + public void gameEvent(ResourceKey p_316780_, BlockPos p_316509_, GameEvent.Context p_316524_) { + this.delegate.gameEvent(p_316780_, p_316509_, p_316524_); + } + + @Override + public boolean isAreaLoaded(BlockPos center, int range) { + return this.delegate.isAreaLoaded(center, range); + } + + @Override + @ApiStatus.NonExtendable + public @Nullable AuxiliaryLightManager getAuxLightManager(BlockPos pos) { + return this.delegate.getAuxLightManager(pos); + } + + @Override + public @Nullable AuxiliaryLightManager getAuxLightManager(ChunkPos pos) { + return this.delegate.getAuxLightManager(pos); + } + + @Override + public ModelData getModelData(BlockPos pos) { + return this.delegate.getModelData(pos); + } + + @Override + public float getShade(float normalX, float normalY, float normalZ, boolean shade) { + return this.delegate.getShade(normalX, normalY, normalZ, shade); + } + + @Override + public boolean noBlockCollision(@Nullable Entity pEntity, AABB pBoundingBox) { + return this.delegate.noBlockCollision(pEntity, pBoundingBox); + } + + @Override + public Optional findSupportingBlock(Entity pEntity, AABB pBox) { + return this.delegate.findSupportingBlock(pEntity, pBox); + } + + @Override + public int getDirectSignalTo(BlockPos pPos) { + return this.delegate.getDirectSignalTo(pPos); + } + + @Override + public int getControlInputSignal(BlockPos pPos, Direction pDirection, boolean pDiodesOnly) { + return this.delegate.getControlInputSignal(pPos, pDirection, pDiodesOnly); + } + + @Override + public boolean hasSignal(BlockPos pPos, Direction pDirection) { + return this.delegate.hasSignal(pPos, pDirection); + } + + @Override + public int getSignal(BlockPos pPos, Direction pDirection) { + return this.delegate.getSignal(pPos, pDirection); + } + + @Override + public boolean hasNeighborSignal(BlockPos pPos) { + return this.delegate.hasNeighborSignal(pPos); + } + + @Override + public int getBestNeighborSignal(BlockPos pPos) { + return this.delegate.getBestNeighborSignal(pPos); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/PreviewLightmapAtlas.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/PreviewLightmapAtlas.java new file mode 100644 index 000000000..ee0029678 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/PreviewLightmapAtlas.java @@ -0,0 +1,110 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import com.mojang.blaze3d.platform.GlConst; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import org.joml.Vector3f; + +import java.util.Objects; + +/** + * 用于结构预览渲染的动态光照贴图构建器。 + */ +public class PreviewLightmapAtlas implements AutoCloseable { + private final DynamicTexture lightmapTexture; + private final NativeImage lightmapPixels; + + public PreviewLightmapAtlas() { + this.lightmapTexture = new DynamicTexture(16, 16, false); + this.lightmapPixels = Objects.requireNonNull(this.lightmapTexture.getPixels()); + this.lightmapPixels.fillRect(0, 0, 16, 16, -1); + this.lightmapTexture.upload(); + } + + /** + * 近似计算当前时间下的原版天空亮度衰减。 + */ + public float computeSkyDarken(Level level, float partialTick) { + var f = level.getTimeOfDay(partialTick); + var g = 1.0F - (Mth.cos(f * (float) (Math.PI * 2)) * 2.0F + 0.2F); + g = Mth.clamp(g, 0.0F, 1.0F); + g = 1.0F - g; + return 0.2f + g * 0.8f; + } + + /** + * 重算并上传 16x16 光照贴图。 + */ + public void update(Level level) { + float f = computeSkyDarken(level, 1.0f); + Vector3f vector3f = new Vector3f(f, f, 1.0F).lerp(new Vector3f(1.0F, 1.0F, 1.0F), 0.35F); + float g = f * 0.95F + 0.05F; + float m = 1.5F; + + var minecraft = Minecraft.getInstance(); + + Vector3f vector3f2 = new Vector3f(); + + for (int skyLightLvl = 0; skyLightLvl < 16; ++skyLightLvl) { + for (int blockLightLvl = 0; blockLightLvl < 16; ++blockLightLvl) { + float p = LightTexture.getBrightness(level.dimensionType(), skyLightLvl) * g; + float q = LightTexture.getBrightness(level.dimensionType(), blockLightLvl) * m; + + float s = q * ((q * 0.6F + 0.4F) * 0.6F + 0.4F); + float t = q * (q * q * 0.6F + 0.4F); + vector3f2.set(q, s, t); + + Vector3f vector3f3 = new Vector3f(vector3f).mul(p); + vector3f2.add(vector3f3); + vector3f2.lerp(new Vector3f(0.75F, 0.75F, 0.75F), 0.04F); + + float v = minecraft.options.gamma().get().floatValue(); + Vector3f vector3f5 = new Vector3f(notGamma(vector3f2.x), notGamma(vector3f2.y), notGamma(vector3f2.z)); + vector3f2.lerp(vector3f5, Math.max(0.0F, v)); + vector3f2.lerp(new Vector3f(0.75F, 0.75F, 0.75F), 0.04F); + clampColor(vector3f2); + vector3f2.mul(255.0F); + + int x = (int) vector3f2.x(); + int y = (int) vector3f2.y(); + int z = (int) vector3f2.z(); + this.lightmapPixels.setPixelRGBA(blockLightLvl, skyLightLvl, 0xFF000000 | z << 16 | y << 8 | x); + } + } + + this.lightmapTexture.upload(); + } + + private static void clampColor(Vector3f vector3f) { + vector3f.set( + Mth.clamp(vector3f.x, 0.0F, 1.0F), Mth.clamp(vector3f.y, 0.0F, 1.0F), + Mth.clamp(vector3f.z, 0.0F, 1.0F) + ); + } + + private float notGamma(float value) { + float f = 1.0F - value; + return 1.0F - f * f * f * f; + } + + /** + * 按原版世界渲染约定,将该光照贴图绑定到纹理单元 2。 + */ + public void bind() { + RenderSystem.setShaderTexture(2, this.lightmapTexture.getId()); + this.lightmapTexture.bind(); + RenderSystem.texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR); + RenderSystem.texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MAG_FILTER, GlConst.GL_LINEAR); + RenderSystem.setShaderColor(1, 1, 1, 1); + } + + @Override + public void close() { + this.lightmapTexture.close(); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxChunkProvider.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxChunkProvider.java new file mode 100644 index 000000000..9db63d2d2 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxChunkProvider.java @@ -0,0 +1,70 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.jspecify.annotations.Nullable; + +import java.util.function.BooleanSupplier; + +/** + * {@link SandboxRenderLevel} 使用的轻量区块来源。 + * + *

区块在首次访问时懒加载创建,并在预览会话期间常驻内存。

+ */ +public class SandboxChunkProvider extends ChunkSource { + private final SandboxRenderLevel level; + + private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + + private final LevelLightEngine lightEngine; + + public SandboxChunkProvider(SandboxRenderLevel level) { + this.level = level; + this.lightEngine = new LevelLightEngine(this, true, true); + } + + /** + * 获取指定坐标的预览区块;若不存在则即时创建。 + */ + @Override + public @Nullable ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus chunkStatus, boolean load) { + var chunkKey = ChunkPos.asLong(chunkX, chunkZ); + var chunk = this.chunks.get(chunkKey); + if (chunk == null) { + chunk = new SandboxLevelChunk(this.level, new ChunkPos(chunkX, chunkZ)); + this.chunks.put(chunkKey, chunk); + } + return chunk; + } + + @Override + public void tick(BooleanSupplier booleanSupplier, boolean b) { + + } + + @Override + public String gatherStats() { + return ""; + } + + @Override + public int getLoadedChunksCount() { + return 0; + } + + @Override + public LevelLightEngine getLightEngine() { + return this.lightEngine; + } + + @Override + public BlockGetter getLevel() { + return this.level; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxLevelChunk.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxLevelChunk.java new file mode 100644 index 000000000..7097de20d --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxLevelChunk.java @@ -0,0 +1,41 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import org.jspecify.annotations.Nullable; + +/** + * 预览区块实现,负责保持 {@link SandboxRenderLevel} 的方块与光照缓存同步。 + */ +public class SandboxLevelChunk extends LevelChunk { + public SandboxLevelChunk(SandboxRenderLevel level, ChunkPos pos) { + super(level, pos); + } + + private SandboxRenderLevel getSandboxLevel() { + return (SandboxRenderLevel) getLevel(); + } + + /** + * 方块状态变化时同步更新光照与非空气方块索引。 + */ + @Nullable + public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving) { + this.getSandboxLevel().refreshLightingAround(pos); + + var result = super.setBlockState(pos, state, isMoving); + if (state.isAir()) { + this.getSandboxLevel().removeFilledBlock(pos); + } else { + this.getSandboxLevel().addFilledBlock(pos); + } + return result; + } + + public FullChunkStatus getFullStatus() { + return FullChunkStatus.FULL; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxRenderLevel.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxRenderLevel.java new file mode 100644 index 000000000..bd36f303a --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/SandboxRenderLevel.java @@ -0,0 +1,484 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import lombok.Getter; +import net.minecraft.Util; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.world.Difficulty; +import net.minecraft.world.TickRateManager; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.item.alchemy.PotionBrewing; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.entity.LevelCallback; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.TransientEntitySectionManager; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.saveddata.maps.MapId; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.ticks.BlackholeTickAccess; +import net.minecraft.world.ticks.LevelTickAccess; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * 用于结构预览的隔离式客户端轻量关卡。 + * + *

只实现渲染与结构放置所需能力,其他与玩法相关系统均采用桩实现。

+ */ +public class SandboxRenderLevel extends Level { + private static final ResourceKey LEVEL_ID = ResourceKey.create( + Registries.DIMENSION, + net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(AnvilLibRendering.MOD_ID, "structure") + ); + + private final TransientEntitySectionManager entitySectionManager = new TransientEntitySectionManager<>( + Entity.class, new LevelCallbackCallbacks() + ); + private final ChunkSource chunkSource = new SandboxChunkProvider(this); + private final Holder biome; + private final RegistryAccess registryAccess; + private final FeatureFlagSet enabledFeatures; + private final LongSet filledBlocks = new LongOpenHashSet(); + private final Map structureBlockEntityData = new HashMap<>(); + private final Map inspectionBlockEntities = new HashMap<>(); + + private final LongSet litSections = new LongOpenHashSet(); + private final DataLayer defaultDataLayer; + + private final TickRateManager tickRateManager = new TickRateManager(); + private final ClientLevel.ClientLevelData clientLevelData; + private final DeltaTracker.Timer tracker = new DeltaTracker.Timer(20.0F, 0L, def -> def); + @Getter + private float partialTick; + + /** 保存放置前的结构数据,防止加载回调改写配置。 */ + void captureBlockEntityData(CompoundTag template, BlockPos placementPos) { + this.structureBlockEntityData.clear(); + this.structureBlockEntityData.putAll(StructureBlockEntityDetails.capture(template, placementPos)); + } + + public @Nullable CompoundTag getStructureBlockEntityData(BlockPos pos) { + CompoundTag data = this.structureBlockEntityData.get(pos); + return data == null ? null : data.copy(); + } + + /** 重新加载保存状态,避免原生加载回调的红石和计时更新影响只读菜单。 */ + public void restoreBlockEntitiesForInspection() { + this.structureBlockEntityData.forEach((pos, data) -> { + BlockEntity entity = BlockEntity.loadStatic(pos, this.getBlockState(pos), data.copy(), this.registryAccess()); + if (entity != null) { + entity.setLevel(this); + this.inspectionBlockEntities.put(pos, entity); + } + }); + } + + @Override + public @Nullable BlockEntity getBlockEntity(BlockPos pos) { + BlockEntity entity = this.inspectionBlockEntities.get(pos); + return entity != null ? entity : super.getBlockEntity(pos); + } + + public SandboxRenderLevel() { + this(Objects.requireNonNull(Minecraft.getInstance().level).registryAccess()); + } + + public SandboxRenderLevel(RegistryAccess registryAccess) { + this(registryAccess, FeatureFlags.DEFAULT_FLAGS); + } + + /** 使用当前世界启用的特性创建沙盒,保证实体类型与预览一致。 */ + public SandboxRenderLevel(RegistryAccess registryAccess, FeatureFlagSet enabledFeatures) { + this(createLevelData(), registryAccess, enabledFeatures); + } + + private SandboxRenderLevel(ClientLevel.ClientLevelData levelData, RegistryAccess registryAccess, FeatureFlagSet enabledFeatures) { + super( + levelData, + LEVEL_ID, + registryAccess, + registryAccess.registryOrThrow(Registries.DIMENSION_TYPE) + .getHolderOrThrow(BuiltinDimensionTypes.OVERWORLD), + () -> InactiveProfiler.INSTANCE, + true, + false, + 0, + 1000000 + ); + this.clientLevelData = levelData; + this.registryAccess = registryAccess; + this.enabledFeatures = enabledFeatures; + this.biome = registryAccess.registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS); + + var nibbles = new byte[DataLayer.SIZE]; + Arrays.fill(nibbles, (byte) 0xFF); + this.defaultDataLayer = new DataLayer(nibbles); + } + + private static ClientLevel.ClientLevelData createLevelData() { + var levelData = new ClientLevel.ClientLevelData(Difficulty.PEACEFUL, false, false); + levelData.setDayTime(6000); + return levelData; + } + + @Override + public void sendBlockUpdated(BlockPos blockPos, BlockState blockState, BlockState blockState1, int i) { + } + + @Override + public void playSeededSound( + @Nullable Player player, + double v, + double v1, + double v2, + Holder holder, + SoundSource soundSource, + float v3, + float v4, + long l + ) { + } + + @Override + public void playSeededSound( + @Nullable Player player, + Entity entity, + Holder holder, + SoundSource soundSource, + float v, + float v1, + long l + ) { + } + + @Override + public String gatherChunkSourceStats() { + return ""; + } + + @Override + protected LevelEntityGetter getEntities() { + return this.entitySectionManager.getEntityGetter(); + } + + @Override + public @Nullable Entity getEntity(int id) { + return this.getEntities().get(id); + } + + /** + * 返回当前预览关卡中可供渲染遍历的实体集合。 + */ + public Iterable getEntitiesForRendering() { + return this.entitySectionManager.getEntityGetter().getAll(); + } + + public void addEntity(Entity entity) { + this.removeEntity(entity.getId(), Entity.RemovalReason.DISCARDED); + this.entitySectionManager.addEntity(entity); + entity.onAddedToLevel(); + this.refreshLightingAround(entity.getOnPos()); + } + + public void removeEntity(int entityId, Entity.RemovalReason reason) { + Entity entity = getEntities().get(entityId); + if (entity != null) { + entity.setRemoved(reason); + entity.onClientRemoval(); + } + } + + public record Bounds(BlockPos min, BlockPos max) { + } + + /** + * 推进游戏时间并计算用于渲染插值的 partialTick。 + */ + public void tickFrameClock() { + var ticksElapsed = tracker.advanceTime(Util.getMillis(), true); + if (ticksElapsed > 0) { + clientLevelData.setGameTime(clientLevelData.getGameTime() + ticksElapsed); + } + + partialTick = tracker.getGameTimeDeltaPartialTick(false); + } + + /** 判断预览是否包含方块或实体,供仅有实体的结构选择投影和边界。 */ + public boolean hasContent() { + return this.hasFilledBlocks() || this.getEntitiesForRendering().iterator().hasNext(); + } + + public boolean hasFilledBlocks() { + return !this.filledBlocks.isEmpty(); + } + + /** + * 返回当前已加载预览区块中所有非空气方块位置。 + */ + public Stream getFilledBlocks() { + var mutablePos = new BlockPos.MutableBlockPos(); + return this.filledBlocks.longStream() + .sequential() + .mapToObj(pos -> { + mutablePos.set(pos); + return mutablePos; + }); + } + + /** + * 计算包围所有已渲染方块与实体的世界坐标轴对齐包围盒。 + */ + public Bounds getBounds() { + if (!this.hasContent()) { + return new Bounds(BlockPos.ZERO, BlockPos.ZERO); + } + + var min = new BlockPos.MutableBlockPos(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + var max = new BlockPos.MutableBlockPos(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + var cur = new BlockPos.MutableBlockPos(); + + this.filledBlocks.forEach(packedPos -> { + cur.set(packedPos); + min.setX(Math.min(min.getX(), cur.getX())); + min.setY(Math.min(min.getY(), cur.getY())); + min.setZ(Math.min(min.getZ(), cur.getZ())); + + max.setX(Math.max(max.getX(), cur.getX() + 1)); + max.setY(Math.max(max.getY(), cur.getY() + 1)); + max.setZ(Math.max(max.getZ(), cur.getZ() + 1)); + }); + + for (var entity : getEntitiesForRendering()) { + var bounds = entity.getBoundingBox(); + + min.setX(Math.min(min.getX(), Mth.floor(bounds.minX))); + min.setY(Math.min(min.getY(), Mth.floor(bounds.minY))); + min.setZ(Math.min(min.getZ(), Mth.floor(bounds.minZ))); + + max.setX(Math.max(max.getX(), (int) Math.ceil(bounds.maxX))); + max.setY(Math.max(max.getY(), (int) Math.ceil(bounds.maxY))); + max.setZ(Math.max(max.getZ(), (int) Math.ceil(bounds.maxZ))); + } + + return new Bounds(min, max); + } + + public boolean isFilledBlock(BlockPos blockPos) { + return this.filledBlocks.contains(blockPos.asLong()); + } + + void removeFilledBlock(BlockPos pos) { + this.filledBlocks.remove(pos.asLong()); + } + + void addFilledBlock(BlockPos pos) { + this.filledBlocks.add(pos.asLong()); + } + + /** + * 预热指定位置周围的光照数据,避免预览渲染出现黑块区域。 + */ + public void refreshLightingAround(BlockPos pos) { + var minChunk = new ChunkPos(pos.offset(-1, -1, -1)); + var maxChunk = new ChunkPos(pos.offset(1, 1, 1)); + ChunkPos.rangeClosed(minChunk, maxChunk).forEach(chunkPos -> { + if (this.litSections.add(chunkPos.toLong())) { + var lightEngine = getLightEngine(); + for (int i = 0; i < getSectionsCount(); ++i) { + int y = getSectionYFromSectionIndex(i); + var sectionPos = SectionPos.of(chunkPos, y); + lightEngine.updateSectionStatus(sectionPos, false); + lightEngine.queueSectionData(LightLayer.BLOCK, sectionPos, this.defaultDataLayer); + lightEngine.queueSectionData(LightLayer.SKY, sectionPos, this.defaultDataLayer); + } + lightEngine.setLightEnabled(chunkPos, true); + lightEngine.propagateLightSources(chunkPos); + lightEngine.retainData(chunkPos, false); + } + }); + } + + @Override + public TickRateManager tickRateManager() { + return this.tickRateManager; + } + + @Override + public @Nullable MapItemSavedData getMapData(MapId mapId) { + return null; + } + + @Override + public void setMapData(MapId mapId, MapItemSavedData mapItemSavedData) { + } + + @Override + public MapId getFreeMapId() { + return new MapId(1); + } + + @Override + public void destroyBlockProgress(int i, BlockPos blockPos, int i1) { + } + + @Override + public Scoreboard getScoreboard() { + return new Scoreboard(); + } + + @Override + public RecipeManager getRecipeManager() { + return Objects.requireNonNull(Minecraft.getInstance().level).getRecipeManager(); + } + + @Override + public PotionBrewing potionBrewing() { + throw new UnsupportedOperationException(); + } + + @Override + public void setDayTimeFraction(float v) { + throw new UnsupportedOperationException(); + } + + @Override + public float getDayTimeFraction() { + throw new UnsupportedOperationException(); + } + + @Override + public float getDayTimePerTick() { + throw new UnsupportedOperationException(); + } + + @Override + public void setDayTimePerTick(float v) { + throw new UnsupportedOperationException(); + } + + @Override + public LevelTickAccess getBlockTicks() { + return BlackholeTickAccess.emptyLevelList(); + } + + @Override + public LevelTickAccess getFluidTicks() { + return BlackholeTickAccess.emptyLevelList(); + } + + @Override + public ChunkSource getChunkSource() { + return this.chunkSource; + } + + @Override + public RegistryAccess registryAccess() { + return this.registryAccess; + } + + @Override + public void levelEvent(@Nullable Player player, int i, BlockPos blockPos, int i1) { + } + + @Override + public void gameEvent(Holder holder, Vec3 vec3, GameEvent.Context context) { + } + + @Override + public float getShade(Direction direction, boolean shade) { + if (!shade) { + return 1.0F; + } else { + return switch (direction) { + case DOWN -> 0.5F; + case NORTH, SOUTH -> 0.8F; + case WEST, EAST -> 0.6F; + default -> 1.0F; + }; + } + } + + @Override + public List players() { + return List.of(); + } + + @Override + public Holder getUncachedNoiseBiome(int i, int i1, int i2) { + return this.biome; + } + + @Override + public FeatureFlagSet enabledFeatures() { + return this.enabledFeatures; + } + + private static class LevelCallbackCallbacks implements LevelCallback { + @Override + public void onCreated(Entity entity) { + } + + @Override + public void onDestroyed(Entity entity) { + } + + @Override + public void onTickingStart(Entity entity) { + } + + @Override + public void onTickingEnd(Entity entity) { + } + + @Override + public void onTrackingStart(Entity entity) { + } + + @Override + public void onTrackingEnd(Entity entity) { + } + + @Override + public void onSectionChange(Entity object) { + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureBlockEntityDetails.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureBlockEntityDetails.java new file mode 100644 index 000000000..57ba28d3c --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureBlockEntityDetails.java @@ -0,0 +1,144 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.locale.Language; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** 从结构快照读取方块实体配置,不依赖模组的客户端同步数据或预览加载回调。 */ +public final class StructureBlockEntityDetails { + private static final String PREFIX = "tooltip.anvillib_rendering.structure_preview."; + private static final Set METADATA = Set.of("id", "x", "y", "z", "keepPacked"); + private static final int MAX_LINES = 4096; + private static final int MAX_DEPTH = 32; + + private StructureBlockEntityDetails() { + } + + /** 在放置结构前复制数据;坐标转换与渲染使用同一原点。 */ + public static Map capture(CompoundTag template, BlockPos placementPos) { + Map result = new HashMap<>(); + for (Tag tag : template.getList("blocks", Tag.TAG_COMPOUND)) { + if (!(tag instanceof CompoundTag block) || !(block.get("nbt") instanceof CompoundTag data)) { + continue; + } + ListTag pos = block.getList("pos", Tag.TAG_INT); + if (pos.size() != 3) { + continue; + } + result.put(placementPos.offset(pos.getInt(0), pos.getInt(1), pos.getInt(2)), data.copy()); + } + return result; + } + + /** 未提供翻译的模组字段保留完整路径和值,嵌套库存、过滤器及组件也可查看。 */ + public static List describe(String blockId, CompoundTag data) { + List lines = new ArrayList<>(); + String blockKey = PREFIX + "nbt." + blockId.replace(':', '.'); + for (String key : data.getAllKeys().stream().sorted().toList()) { + if (!METADATA.contains(key)) { + append(lines, blockKey, key, data.get(key), 0); + } + } + if (lines.isEmpty()) { + lines.add(Component.translatable(PREFIX + "empty_block_entity")); + } + return List.copyOf(lines); + } + + private static void append(List lines, String blockKey, String path, Tag tag, int depth) { + if (lines.size() >= MAX_LINES) { + if (lines.size() == MAX_LINES) { + lines.add(Component.translatable(PREFIX + "data_limit")); + } + return; + } + if (depth >= MAX_DEPTH) { + lines.add(Component.literal(path + ": ").append(Component.translatable(PREFIX + "data_limit"))); + return; + } + if (tag instanceof CompoundTag compound && !compound.isEmpty()) { + for (String key : compound.getAllKeys().stream().sorted().toList()) { + append(lines, blockKey, path + "." + key, compound.get(key), depth + 1); + if (lines.size() > MAX_LINES) { + break; + } + } + } else if (tag instanceof ListTag list && !list.isEmpty()) { + for (int i = 0; i < list.size() && lines.size() <= MAX_LINES; i++) { + append(lines, blockKey, path + "[" + i + "]", list.get(i), depth + 1); + } + } else if (tag instanceof ByteArrayTag array) { + appendArray(lines, blockKey, path, array.getAsByteArray().length, i -> Byte.toString(array.getAsByteArray()[i])); + } else if (tag instanceof IntArrayTag array) { + appendArray(lines, blockKey, path, array.getAsIntArray().length, i -> Integer.toString(array.getAsIntArray()[i])); + } else if (tag instanceof LongArrayTag array) { + appendArray(lines, blockKey, path, array.getAsLongArray().length, i -> Long.toString(array.getAsLongArray()[i])); + } else { + String value = tag instanceof StringTag string ? string.getAsString() : tag.toString(); + if (tag instanceof NumericTag) { + value = value.replaceFirst("[bBsSlLfFdD]$", ""); + } + lines.add(field(blockKey, path, value)); + } + } + + private static void appendArray(List lines, String blockKey, String path, int size, + java.util.function.IntFunction value) { + if (size == 0) { + lines.add(field(blockKey, path, "[]")); + } + for (int i = 0; i < size; i++) { + if (lines.size() >= MAX_LINES) { + lines.add(Component.translatable(PREFIX + "data_limit")); + break; + } + lines.add(field(blockKey, path + "[" + i + "]", value.apply(i))); + } + } + + private static Component field(String blockKey, String path, String value) { + String fieldKey = blockKey + "." + path; + Language language = Language.getInstance(); + MutableComponent label = Component.empty(); + for (String segment : path.split("\\.")) { + if (!label.getSiblings().isEmpty()) { + label.append("."); + } + int index = segment.indexOf('['); + String name = index < 0 ? segment : segment.substring(0, index); + label.append(Component.translatableWithFallback(PREFIX + "nbt.field." + name, name)); + if (index >= 0) { + label.append(segment.substring(index)); + } + } + if (language.has(fieldKey)) { + label = Component.translatable(fieldKey); + } + String leaf = path.substring(path.lastIndexOf('.') + 1); + String valueKey = fieldKey + ".value." + value; + if (!language.has(valueKey)) { + valueKey = PREFIX + "nbt.field." + leaf + ".value." + value; + } + Component displayValue = Component.translatableWithFallback(valueKey, value); + if (language.has(fieldKey + ".format")) { + displayValue = Component.translatable(fieldKey + ".format", displayValue); + } + return label.copy().append(": ").append(displayValue).withStyle(ChatFormatting.GRAY); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureDetailsPager.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureDetailsPager.java new file mode 100644 index 000000000..6b8958d7e --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureDetailsPager.java @@ -0,0 +1,74 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** 按实际字体宽度换行并翻页,保证较长的方块实体配置不会超出窗口。 */ +public final class StructureDetailsPager { + private List source = List.of(); + private List wrapped = List.of(); + private int width; + private int rows = 10; + private int page; + + public void setLines(List lines) { + this.source = List.copyOf(lines); + this.wrapped = List.of(); + this.width = 0; + this.page = 0; + } + + public List visible(Font font, int screenWidth, int screenHeight) { + int nextWidth = Math.max(1, Math.min(320, screenWidth - 24)); + int nextRows = Math.max(1, Math.min(12, (screenHeight - 24) / (font.lineHeight + 1) - 2)); + if (this.width != nextWidth || this.rows != nextRows) { + this.width = nextWidth; + this.rows = nextRows; + this.page = 0; + List result = new ArrayList<>(); + for (Component line : this.source) { + for (var sequence : font.getSplitter().splitLines(line, this.width, Style.EMPTY)) { + MutableComponent text = Component.empty(); + sequence.visit((style, value) -> { + text.append(Component.literal(value).withStyle(style)); + return Optional.empty(); + }, Style.EMPTY); + result.add(text); + } + } + this.wrapped = List.copyOf(result); + } + return page(this.wrapped, this.page, this.rows); + } + + static List page(List lines, int page, int rows) { + if (lines.isEmpty()) { + return List.of(); + } + rows = Math.max(1, rows); + int pages = (lines.size() + rows - 1) / rows; + page = Math.max(0, Math.min(pages - 1, page)); + int start = page * rows; + List result = new ArrayList<>(lines.subList(start, Math.min(start + rows, lines.size()))); + if (pages > 1) { + result.add(Component.translatable("tooltip.anvillib_rendering.structure_preview.page", page + 1, pages) + .withStyle(ChatFormatting.YELLOW)); + } + return result; + } + + public void scroll(double amount) { + this.page = Math.max(0, Math.min(this.pages() - 1, this.page + (amount < 0 ? 1 : -1))); + } + + private int pages() { + return Math.max(1, (this.wrapped.size() + this.rows - 1) / this.rows); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewPicker.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewPicker.java new file mode 100644 index 000000000..e75af1243 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewPicker.java @@ -0,0 +1,77 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.Shapes; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** 结构预览的方块轮廓选取与只读方块信息。 */ +public final class StructurePreviewPicker { + private StructurePreviewPicker() { + } + + /** 沿预览相机射线选取最近的可见方块;隐藏层不会挡住下层方块。 */ + public static @Nullable BlockPos pick(SandboxRenderLevel level, Vec3 start, Vec3 end, int minY, int maxY) { + BlockPos selected = null; + double nearest = Double.POSITIVE_INFINITY; + var positions = level.getFilledBlocks().iterator(); + while (positions.hasNext()) { + BlockPos pos = positions.next(); + if (pos.getY() < minY || pos.getY() >= maxY) { + continue; + } + BlockState state = level.getBlockState(pos); + if (state.isAir() || state.getRenderShape() == RenderShape.INVISIBLE && state.getFluidState().isEmpty()) { + continue; + } + var shape = state.getShape(level, pos); + // 流体等没有方块轮廓的可见内容也应能查看配置。 + if (shape.isEmpty()) { + shape = Shapes.block(); + } + var hit = shape.clip(start, end, pos); + if (hit == null) { + continue; + } + double distance = start.distanceToSqr(hit.getLocation()); + if (distance < nearest) { + nearest = distance; + selected = pos.immutable(); + } + } + return selected; + } + + /** 名称使用游戏翻译,坐标相对于结构原点,属性值保持原版序列化名称。 */ + public static List tooltip(SandboxRenderLevel level, BlockPos pos, BlockPos placementPos) { + BlockState state = level.getBlockState(pos); + List lines = new ArrayList<>(); + lines.add(state.getBlock().getName().copy().withStyle(ChatFormatting.GOLD)); + lines.add(Component.literal(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString()).withStyle(ChatFormatting.GRAY)); + BlockPos relative = pos.subtract(placementPos); + lines.add(Component.translatable("tooltip.anvillib_rendering.structure_preview.position", relative.getX(), relative.getY(), relative.getZ())); + if (state.getProperties().isEmpty()) { + lines.add(Component.translatable("tooltip.anvillib_rendering.structure_preview.empty_properties").withStyle(ChatFormatting.GRAY)); + } else { + lines.add(Component.translatable("tooltip.anvillib_rendering.structure_preview.properties")); + state.getProperties().stream().sorted(Comparator.comparing(Property::getName)) + .forEach(property -> lines.add(propertyLine(state, property))); + } + return List.copyOf(lines); + } + + private static > Component propertyLine(BlockState state, Property property) { + return Component.literal(property.getName() + " = " + property.getName(state.getValue(property))) + .withStyle(ChatFormatting.GRAY); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewRenderer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewRenderer.java new file mode 100644 index 000000000..120ffbcfe --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructurePreviewRenderer.java @@ -0,0 +1,441 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import com.mojang.blaze3d.platform.GlConst; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.shaders.FogShape; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexSorting; +import dev.anvilcraft.lib.v2.rendering.preview.SectionOffsetVertexConsumer; +import dev.anvilcraft.lib.v2.rendering.preview.SodiumSpriteBridge; +import dev.anvilcraft.lib.v2.rendering.preview.ViewportCameraRig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.Sheets; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.ColorResolver; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions; +import net.neoforged.neoforge.client.model.data.ModelData; +import org.joml.Matrix4f; +import org.joml.Vector4f; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +/** + * 将 {@link SandboxRenderLevel} 渲染到当前 GUI 渲染目标。 + * + *

该渲染器在保持轻量单关卡预览管线的同时,尽量对齐原版世界渲染顺序, + * 以减少渲染异常。

+ */ +public class StructurePreviewRenderer { + /** 调用方可在结构之后绘制模型及覆盖效果,不需要注入渲染器。 */ + public interface Effects { + Effects NONE = new Effects() {}; + + default void submit(MultiBufferSource.BufferSource buffers) { + } + + default void render() { + } + } + + private static @Nullable StructurePreviewRenderer instance; + + private final PreviewLightmapAtlas lightmap = new PreviewLightmapAtlas(); + + public static StructurePreviewRenderer getInstance() { + RenderSystem.assertOnRenderThread(); + if (StructurePreviewRenderer.instance == null) { + StructurePreviewRenderer.instance = new StructurePreviewRenderer(); + } + return StructurePreviewRenderer.instance; + } + + /** + * 使用内部持有的缓冲源渲染关卡。 + */ + public void render( + SandboxRenderLevel level, + ViewportCameraRig cameraRig + ) { + lightmap.update(level); + + RenderSystem.clear(GlConst.GL_DEPTH_BUFFER_BIT, Minecraft.ON_OSX); + + level.tickFrameClock(); + + RenderSystem.setShaderGameTime(level.getGameTime(), level.getPartialTick()); + + var buffers = Minecraft.getInstance().renderBuffers().bufferSource(); + render(level, cameraRig, buffers); + buffers.endBatch(); + } + + /** + * 使用调用方提供的缓冲源渲染关卡。 + */ + public void render( + SandboxRenderLevel level, + ViewportCameraRig cameraRig, + MultiBufferSource.BufferSource buffers + ) { + this.render(level, cameraRig, buffers, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + /** + * 使用调用方提供的缓冲源渲染指定可见层范围内的关卡内容。 + */ + public void render( + SandboxRenderLevel level, + ViewportCameraRig cameraRig, + MultiBufferSource.BufferSource buffers, + int visibleMinY, + int visibleMaxYExclusive + ) { + this.render(level, cameraRig, buffers, visibleMinY, visibleMaxYExclusive, Effects.NONE); + } + + /** 在预览相机矩阵有效期间绘制额外内容,随后恢复 GUI 绘制状态。 */ + public void render(SandboxRenderLevel level, ViewportCameraRig cameraRig, MultiBufferSource.BufferSource buffers, + int visibleMinY, int visibleMaxYExclusive, Effects effects) { + java.util.Objects.requireNonNull(effects); + lightmap.update(level); + + // 先清空待处理光照任务,确保本帧光照采样稳定。 + var lightEngine = level.getLightEngine(); + while (lightEngine.hasLightWork()) { + lightEngine.runLightUpdates(); + } + + // 安装预览矩阵并关闭雾效,保证指南页面渲染结果稳定。 + var projectionMatrix = cameraRig.buildProjectionMatrix(); + var viewMatrix = cameraRig.buildViewMatrix(); + + // 基本等价于关闭关卡雾效 + RenderSystem.setShaderFogColor(1, 1, 1, 0); + RenderSystem.setShaderFogStart(0); + RenderSystem.setShaderFogEnd(1000); + RenderSystem.setShaderFogShape(FogShape.SPHERE); + + var modelViewStack = RenderSystem.getModelViewStack(); + modelViewStack.pushMatrix(); + modelViewStack.identity(); + modelViewStack.mul(viewMatrix); + RenderSystem.applyModelViewMatrix(); + RenderSystem.backupProjectionMatrix(); + RenderSystem.setProjectionMatrix(projectionMatrix, VertexSorting.ORTHOGRAPHIC_Z); + + var lightDirection = new Vector4f(15 / 90f, .35f, 1, 0); + var lightTransform = new Matrix4f(viewMatrix); + lightTransform.invert(); + lightTransform.transform(lightDirection); + + Lighting.setupLevel(); + + try { + renderContent(level, buffers, visibleMinY, visibleMaxYExclusive, effects); + // 自定义方块实体也可能使用上述标准渲染层以外的缓冲,必须在恢复 GUI 矩阵前提交。 + buffers.endBatch(); + + effects.render(); + } finally { + modelViewStack.popMatrix(); + RenderSystem.applyModelViewMatrix(); + RenderSystem.restoreProjectionMatrix(); + + Lighting.setupFor3DItems(); + } + } + + /** + * 按接近原版关卡渲染的顺序执行各个绘制阶段。 + */ + public void renderContent(SandboxRenderLevel level, MultiBufferSource.BufferSource buffers) { + this.renderContent(level, buffers, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + /** + * 按接近原版关卡渲染的顺序执行各个绘制阶段,并限制可见层范围。 + */ + public void renderContent( + SandboxRenderLevel level, + MultiBufferSource.BufferSource buffers, + int visibleMinY, + int visibleMaxYExclusive + ) { + this.renderContent(level, buffers, visibleMinY, visibleMaxYExclusive, Effects.NONE); + } + + private void renderContent(SandboxRenderLevel level, MultiBufferSource.BufferSource buffers, + int visibleMinY, int visibleMaxYExclusive, Effects effects) { + RenderSystem.runAsFancy(() -> { + // 第一阶段:先绘制不透明内容与实体系统。 + renderBlocks(level, buffers, false, visibleMinY, visibleMaxYExclusive); + renderBlockEntities(level, buffers, level.getPartialTick(), visibleMinY, visibleMaxYExclusive); + renderEntities(level, buffers, level.getPartialTick(), visibleMinY, visibleMaxYExclusive); + effects.submit(buffers); + + // 该顺序参考 LevelRenderer#renderLevel + buffers.endBatch(RenderType.entitySolid(TextureAtlas.LOCATION_BLOCKS)); + buffers.endBatch(RenderType.entityCutout(TextureAtlas.LOCATION_BLOCKS)); + buffers.endBatch(RenderType.entityCutoutNoCull(TextureAtlas.LOCATION_BLOCKS)); + buffers.endBatch(RenderType.entitySmoothCutout(TextureAtlas.LOCATION_BLOCKS)); + + // 这些层在原版通常已预烘焙,这里需要手动结束批次 + for (var layer : RenderType.chunkBufferLayers()) { + if (layer != RenderType.translucent()) { + buffers.endBatch(layer); + } + } + + buffers.endBatch(RenderType.solid()); + buffers.endBatch(RenderType.endPortal()); + buffers.endBatch(RenderType.endGateway()); + buffers.endBatch(Sheets.solidBlockSheet()); + buffers.endBatch(Sheets.cutoutBlockSheet()); + buffers.endBatch(Sheets.bedSheet()); + buffers.endBatch(Sheets.shulkerBoxSheet()); + buffers.endBatch(Sheets.signSheet()); + buffers.endBatch(Sheets.hangingSignSheet()); + buffers.endBatch(Sheets.chestSheet()); + buffers.endLastBatch(); + + // 第二阶段:在不透明缓冲全部提交后绘制半透明方块。 + renderBlocks(level, buffers, true, visibleMinY, visibleMaxYExclusive); + buffers.endBatch(RenderType.translucent()); + }); + } + + private void renderBlocks( + SandboxRenderLevel level, + MultiBufferSource buffers, + boolean translucent, + int visibleMinY, + int visibleMaxYExclusive + ) { + var randomSource = level.random; + var blockRenderDispatcher = Minecraft.getInstance().getBlockRenderer(); + var poseStack = new PoseStack(); + var layerView = new VisibleLayerBlockAndTintGetter(level, visibleMinY, visibleMaxYExclusive); + + level.getFilledBlocks().forEach(pos -> { + if (!isVisibleLayer(pos.getY(), visibleMinY, visibleMaxYExclusive)) { + return; + } + + var blockState = level.getBlockState(pos); + var fluidState = blockState.getFluidState(); + if (!fluidState.isEmpty()) { + var renderType = ItemBlockRenderTypes.getRenderLayer(fluidState); + if ((renderType == RenderType.translucent()) == translucent) { + var bufferBuilder = buffers.getBuffer(renderType); + + var sectionPos = SectionPos.of(pos); + var sectionOffsetWriter = new SectionOffsetVertexConsumer(bufferBuilder, sectionPos); + blockRenderDispatcher.renderLiquid(pos, layerView, sectionOffsetWriter, blockState, fluidState); + + markFluidSpritesActive(fluidState); + } + } + + if (blockState.getRenderShape() != RenderShape.INVISIBLE) { + var be = level.getBlockEntity(pos); + ModelData modelData = ModelData.EMPTY; + if (be != null) { + modelData = be.getModelData(); + } + + var model = blockRenderDispatcher.getBlockModel(blockState); + modelData = model.getModelData(layerView, pos, blockState, modelData); + var renderTypes = model.getRenderTypes(blockState, randomSource, modelData); + + for (var renderType : renderTypes) { + if ((renderType == RenderType.translucent()) == translucent) { + var bufferBuilder = buffers.getBuffer(renderType); + + poseStack.pushPose(); + poseStack.translate(pos.getX(), pos.getY(), pos.getZ()); + blockRenderDispatcher.renderBatched( + blockState, + pos, + layerView, + poseStack, + bufferBuilder, + true, + randomSource, + modelData, + renderType + ); + poseStack.popPose(); + } + } + } + }); + } + + private void renderBlockEntities( + SandboxRenderLevel level, + MultiBufferSource buffers, + float partialTick, + int visibleMinY, + int visibleMaxYExclusive + ) { + var poseStack = new PoseStack(); + + level.getFilledBlocks().forEach(pos -> { + if (!isVisibleLayer(pos.getY(), visibleMinY, visibleMaxYExclusive)) { + return; + } + + var blockState = level.getBlockState(pos); + if (blockState.hasBlockEntity()) { + var blockEntity = level.getBlockEntity(pos); + if (blockEntity != null) { + this.handleBlockEntity(poseStack, blockEntity, buffers, partialTick); + } + } + }); + } + public static void markFluidSpritesActive(FluidState fluidState) { + var props = IClientFluidTypeExtensions.of(fluidState); + var sprite1 = Minecraft.getInstance().getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(props.getStillTexture()); + SodiumSpriteBridge.pingSpriteUsage(sprite1); + var sprite2 = Minecraft.getInstance().getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(props.getFlowingTexture()); + SodiumSpriteBridge.pingSpriteUsage(sprite2); + } + + private void handleBlockEntity(PoseStack stack, E blockEntity, MultiBufferSource buffers, float partialTicks) { + var dispatcher = Minecraft.getInstance().getBlockEntityRenderDispatcher(); + var renderer = dispatcher.getRenderer(blockEntity); + if (renderer != null && renderer.shouldRender(blockEntity, blockEntity.getBlockPos().getCenter())) { + var pos = blockEntity.getBlockPos(); + stack.pushPose(); + stack.translate(pos.getX(), pos.getY(), pos.getZ()); + + int packedLight = LevelRenderer.getLightColor(Objects.requireNonNull(blockEntity.getLevel()), blockEntity.getBlockPos()); + renderer.render(blockEntity, partialTicks, stack, buffers, packedLight, OverlayTexture.NO_OVERLAY); + stack.popPose(); + } + } + + private void renderEntities( + SandboxRenderLevel level, + MultiBufferSource.BufferSource buffers, + float partialTick, + int visibleMinY, + int visibleMaxYExclusive + ) { + var poseStack = new PoseStack(); + + for (var entity : level.getEntitiesForRendering()) { + if (entity.getBoundingBox().maxY > visibleMinY && entity.getBoundingBox().minY < visibleMaxYExclusive) { + handleEntity(level, poseStack, entity, buffers, partialTick); + } + } + } + + private static boolean isVisibleLayer(int y, int visibleMinY, int visibleMaxYExclusive) { + return y >= visibleMinY && y < visibleMaxYExclusive; + } + + private record VisibleLayerBlockAndTintGetter( + SandboxRenderLevel delegate, + int visibleMinY, + int visibleMaxYExclusive + ) implements BlockAndTintGetter { + private boolean isVisible(BlockPos pos) { + return isVisibleLayer(pos.getY(), this.visibleMinY, this.visibleMaxYExclusive); + } + + @Override + @Nullable + public BlockEntity getBlockEntity(BlockPos pos) { + return this.isVisible(pos) ? this.delegate.getBlockEntity(pos) : null; + } + + @Override + public BlockState getBlockState(BlockPos pos) { + return this.isVisible(pos) ? this.delegate.getBlockState(pos) : Blocks.AIR.defaultBlockState(); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return this.isVisible(pos) ? this.delegate.getFluidState(pos) : Fluids.EMPTY.defaultFluidState(); + } + + @Override + public float getShade(Direction direction, boolean shade) { + return this.delegate.getShade(direction, shade); + } + + @Override + public LevelLightEngine getLightEngine() { + return this.delegate.getLightEngine(); + } + + @Override + public int getBlockTint(BlockPos blockPos, ColorResolver colorResolver) { + return this.delegate.getBlockTint(blockPos, colorResolver); + } + + @Override + public int getBrightness(LightLayer lightLayer, BlockPos blockPos) { + return this.delegate.getBrightness(lightLayer, blockPos); + } + + @Override + public int getRawBrightness(BlockPos blockPos, int amount) { + return this.delegate.getRawBrightness(blockPos, amount); + } + + @Override + public int getHeight() { + return this.delegate.getHeight(); + } + + @Override + public int getMinBuildHeight() { + return this.delegate.getMinBuildHeight(); + } + } + + private void handleEntity( + SandboxRenderLevel level, + PoseStack poseStack, + E entity, + MultiBufferSource buffers, + float partialTicks + ) { + var dispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + var renderer = dispatcher.getRenderer(entity); + + var probePos = BlockPos.containing(entity.getLightProbePosition(partialTicks)); + int packedLight = LevelRenderer.getLightColor(level, probePos); + var yaw = entity.getYRot(); + + var pos = entity.position(); + var offset = renderer.getRenderOffset(entity, partialTicks); + poseStack.pushPose(); + poseStack.translate(pos.x + offset.x(), pos.y + offset.y(), pos.z + offset.z()); + renderer.render(entity, yaw, partialTicks, poseStack, buffers, packedLight); + poseStack.popPose(); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureSandboxFactory.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureSandboxFactory.java new file mode 100644 index 000000000..6d5573791 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/StructureSandboxFactory.java @@ -0,0 +1,109 @@ +package dev.anvilcraft.lib.v2.rendering.preview.level; + +import lombok.extern.slf4j.Slf4j; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.SingleThreadedRandomSource; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * 将 {@link StructureTemplate} 放入客户端沙盒关卡的辅助工厂。 + */ +@Slf4j +public final class StructureSandboxFactory { + private StructureSandboxFactory() { + } + + /** + * 使用给定的模板放置原点创建一个新的沙盒预览关卡。 + */ + public static @Nullable SandboxRenderLevel create(@Nullable Level clientLevel, StructureTemplate template, BlockPos placementPos) { + if (clientLevel == null) { + return null; + } + + SingleThreadedRandomSource random = new SingleThreadedRandomSource(0L); + StructurePlaceSettings settings = new StructurePlaceSettings(); + settings.setIgnoreEntities(true); + // 保留蓝图中的连接状态,避免邻接更新改变预览结构。 + settings.setKnownShape(true); + + SandboxRenderLevel level = new SandboxRenderLevel(clientLevel.registryAccess(), clientLevel.enabledFeatures()); + CompoundTag structureData = template.save(new CompoundTag()); + level.captureBlockEntityData(structureData, placementPos); + DelegatingServerLevelAccessor fakeServerLevel = new DelegatingServerLevelAccessor(level); + boolean placed = template.placeInWorld(fakeServerLevel, placementPos, placementPos, settings, random, Block.UPDATE_KNOWN_SHAPE); + // 原版结构实体加载依赖服务端关卡,预览在客户端沙盒中重建实体。 + for (Tag entityTag : structureData.getList("entities", Tag.TAG_COMPOUND)) { + CompoundTag entry = (CompoundTag) entityTag; + CompoundTag entityData = entry.getCompound("nbt").copy(); + ListTag relativePos = entry.getList("pos", Tag.TAG_DOUBLE); + ListTag originalPos = entityData.getList("Pos", Tag.TAG_DOUBLE); + Vec3 offset = new Vec3( + relativePos.getDouble(0) + placementPos.getX() - originalPos.getDouble(0), + relativePos.getDouble(1) + placementPos.getY() - originalPos.getDouble(1), + relativePos.getDouble(2) + placementPos.getZ() - originalPos.getDouble(2) + ); + relocateEntityData(entityData, offset); + Entity entity = EntityType.loadEntityRecursive(entityData, level, loaded -> { + if (loaded instanceof FallingBlockEntity falling) { + falling.setStartPos(falling.blockPosition()); + } + return loaded; + }); + if (entity != null) { + entity.getSelfAndPassengers().forEach(level::addEntity); + } + } + if (!placed && !level.hasContent()) { + log.debug("Failed to place structure preview into sandbox at {}", placementPos); + } + if (level.getEntitiesForRendering().iterator().hasNext()) { + level.getLightEngine().runLightUpdates(); + } + return level; + } + + static void relocateEntityData(CompoundTag data, Vec3 offset) { + ListTag original = data.getList("Pos", Tag.TAG_DOUBLE); + ListTag position = new ListTag(); + position.add(DoubleTag.valueOf(original.getDouble(0) + offset.x)); + position.add(DoubleTag.valueOf(original.getDouble(1) + offset.y)); + position.add(DoubleTag.valueOf(original.getDouble(2) + offset.z)); + data.put("Pos", position); + data.remove("UUID"); + // 1.21 的悬挂实体用整数坐标保存附着位置。 + if (data.contains("TileX", Tag.TAG_ANY_NUMERIC) + && data.contains("TileY", Tag.TAG_ANY_NUMERIC) + && data.contains("TileZ", Tag.TAG_ANY_NUMERIC)) { + data.putInt("TileX", data.getInt("TileX") + (int) Math.round(offset.x)); + data.putInt("TileY", data.getInt("TileY") + (int) Math.round(offset.y)); + data.putInt("TileZ", data.getInt("TileZ") + (int) Math.round(offset.z)); + } + for (Tag passenger : data.getList("Passengers", Tag.TAG_COMPOUND)) { + relocateEntityData((CompoundTag) passenger, offset); + } + } + + /** + * 返回适合 GUI 预览的居中放置原点。 + */ + public static BlockPos centeredPlacement(StructureTemplate template) { + Vec3i size = template.getSize(); + int x = (int) Math.ceil(size.getX() / 2.0d); + int z = (int) Math.ceil(size.getZ() / 2.0d); + return new BlockPos(-x, 0, -z); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/package-info.java new file mode 100644 index 000000000..00afbf0ed --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/level/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package dev.anvilcraft.lib.v2.rendering.preview.level; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/package-info.java new file mode 100644 index 000000000..3532331b4 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/preview/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package dev.anvilcraft.lib.v2.rendering.preview; diff --git a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml index 516f9ec38..cf992a9a4 100644 --- a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml +++ b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml @@ -80,3 +80,6 @@ side = "BOTH" # stop your mod loading on the server for example. #[features."${mod_id}"] #openGLVersion="[3.2,)" + +[[mixins]] +config="anvillib_rendering.mixins.json" diff --git a/module.rendering/src/main/resources/anvillib_rendering.mixins.json b/module.rendering/src/main/resources/anvillib_rendering.mixins.json new file mode 100644 index 000000000..8ab77d951 --- /dev/null +++ b/module.rendering/src/main/resources/anvillib_rendering.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.rendering.mixins", + "compatibilityLevel": "JAVA_21", + "client": [ + "preview.StructureGuiGraphicsMixin", + "preview.StructureGuiRecipeBookMixin", + "preview.StructureGuiRenderStateMixin", + "preview.StructureGuiScreenMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/lang/en_us.json b/module.rendering/src/main/resources/assets/anvillib_rendering/lang/en_us.json new file mode 100644 index 000000000..1014cfaf6 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/lang/en_us.json @@ -0,0 +1,109 @@ +{ + "tooltip.anvillib_rendering.structure_preview.block_entity": "Block entity data (saved in structure):", + "tooltip.anvillib_rendering.structure_preview.data_limit": "Data limit reached; remaining entries not expanded", + "tooltip.anvillib_rendering.structure_preview.empty_block_entity": "No additional configuration saved", + "tooltip.anvillib_rendering.structure_preview.empty_properties": "This block has no state properties", + "tooltip.anvillib_rendering.structure_preview.inspect": "Click to inspect", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Amount": "Amount", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Base": "Base", + "tooltip.anvillib_rendering.structure_preview.nbt.field.BrewTime": "Brewing time", + "tooltip.anvillib_rendering.structure_preview.nbt.field.BurnTime": "Burn time", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Command": "Command", + "tooltip.anvillib_rendering.structure_preview.nbt.field.CookTime": "Cook time", + "tooltip.anvillib_rendering.structure_preview.nbt.field.CookTimeTotal": "Total cook time", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Cooldown": "Cooldown", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Count": "Count", + "tooltip.anvillib_rendering.structure_preview.nbt.field.CustomName": "Custom name", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Delay": "Delay", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Disabled": "Disabled", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Disabled.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Disabled.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Energy": "Energy", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Enhanced": "Enhanced", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Enhanced.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Enhanced.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.EntityId": "Entity ID", + "tooltip.anvillib_rendering.structure_preview.nbt.field.ExtraData": "Configuration", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Faces": "Faces", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Filter": "Filter", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterEnabled": "Filter enabled", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterEnabled.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterEnabled.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterMode": "Match mode", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Filtering": "Filtering", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Fluid": "Fluid", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FluidName": "Fluid ID", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Fuel": "Fuel", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Infinite": "Infinite", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Infinite.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Infinite.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.InputPower": "Input power", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Inventory": "Inventory", + "tooltip.anvillib_rendering.structure_preview.nbt.field.IsEmptySlot": "Empty slot", + "tooltip.anvillib_rendering.structure_preview.nbt.field.IsEmptySlot.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.IsEmptySlot.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Items": "Items", + "tooltip.anvillib_rendering.structure_preview.nbt.field.LastOutput": "Last output", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Lock": "Lock", + "tooltip.anvillib_rendering.structure_preview.nbt.field.LootTable": "Loot table", + "tooltip.anvillib_rendering.structure_preview.nbt.field.LootTableSeed": "Loot seed", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Output": "Output signal", + "tooltip.anvillib_rendering.structure_preview.nbt.field.OutputMode": "Output mode", + "tooltip.anvillib_rendering.structure_preview.nbt.field.OutputSignal": "Output signal", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Patterns": "Patterns", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Range": "Detection range", + "tooltip.anvillib_rendering.structure_preview.nbt.field.RangeRadius": "Range radius", + "tooltip.anvillib_rendering.structure_preview.nbt.field.RecipesUsed": "Recipes used", + "tooltip.anvillib_rendering.structure_preview.nbt.field.RecordItem": "Record", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Size": "Size", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Slot": "Slot", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterEnabled": "Slot filter enabled", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterEnabled.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterEnabled.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterItem": "Marked item", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotItem": "Slot item", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotLimit": "Slot limit", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SpawnData": "Spawn data", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SpawnPotentials": "Spawn potentials", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Tank": "Tank", + "tooltip.anvillib_rendering.structure_preview.nbt.field.TransferCooldown": "Transfer cooldown", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Uniform": "Uniform mode", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Uniform.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Uniform.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.amount": "Amount", + "tooltip.anvillib_rendering.structure_preview.nbt.field.auto": "Automatic", + "tooltip.anvillib_rendering.structure_preview.nbt.field.back_text": "Back text", + "tooltip.anvillib_rendering.structure_preview.nbt.field.color": "Color", + "tooltip.anvillib_rendering.structure_preview.nbt.field.components": "Components", + "tooltip.anvillib_rendering.structure_preview.nbt.field.count": "Count", + "tooltip.anvillib_rendering.structure_preview.nbt.field.filtered_messages": "Filtered messages", + "tooltip.anvillib_rendering.structure_preview.nbt.field.front_text": "Front text", + "tooltip.anvillib_rendering.structure_preview.nbt.field.gamma": "Gamma mode", + "tooltip.anvillib_rendering.structure_preview.nbt.field.gamma.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.gamma.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.has_glowing_text": "Glowing text", + "tooltip.anvillib_rendering.structure_preview.nbt.field.has_glowing_text.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.has_glowing_text.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.id": "ID", + "tooltip.anvillib_rendering.structure_preview.nbt.field.is_waxed": "Waxed", + "tooltip.anvillib_rendering.structure_preview.nbt.field.is_waxed.value.0": "No", + "tooltip.anvillib_rendering.structure_preview.nbt.field.is_waxed.value.1": "Yes", + "tooltip.anvillib_rendering.structure_preview.nbt.field.items": "Items", + "tooltip.anvillib_rendering.structure_preview.nbt.field.laserLevel": "Laser level", + "tooltip.anvillib_rendering.structure_preview.nbt.field.lensType": "Lens type", + "tooltip.anvillib_rendering.structure_preview.nbt.field.loadedStructureName": "Structure name", + "tooltip.anvillib_rendering.structure_preview.nbt.field.marked_item": "Marked item", + "tooltip.anvillib_rendering.structure_preview.nbt.field.messages": "Messages", + "tooltip.anvillib_rendering.structure_preview.nbt.field.operation": "Operation", + "tooltip.anvillib_rendering.structure_preview.nbt.field.patterns": "Patterns", + "tooltip.anvillib_rendering.structure_preview.nbt.field.placement": "Placement", + "tooltip.anvillib_rendering.structure_preview.nbt.field.rangeX": "X range", + "tooltip.anvillib_rendering.structure_preview.nbt.field.rangeY": "Y range", + "tooltip.anvillib_rendering.structure_preview.nbt.field.rangeZ": "Z range", + "tooltip.anvillib_rendering.structure_preview.nbt.field.storedEnergy": "Stored energy", + "tooltip.anvillib_rendering.structure_preview.nbt.field.target": "Target", + "tooltip.anvillib_rendering.structure_preview.no_gui": "No preview available", + "tooltip.anvillib_rendering.structure_preview.page": "%s/%s - Shift + wheel", + "tooltip.anvillib_rendering.structure_preview.position": "Structure position: %s, %s, %s", + "tooltip.anvillib_rendering.structure_preview.properties": "Block state:" +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/lang/zh_cn.json b/module.rendering/src/main/resources/assets/anvillib_rendering/lang/zh_cn.json new file mode 100644 index 000000000..9ba1ba3bf --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/lang/zh_cn.json @@ -0,0 +1,109 @@ +{ + "tooltip.anvillib_rendering.structure_preview.inspect": "点击查看", + "tooltip.anvillib_rendering.structure_preview.position": "结构坐标:%s, %s, %s", + "tooltip.anvillib_rendering.structure_preview.properties": "方块状态:", + "tooltip.anvillib_rendering.structure_preview.empty_properties": "此方块没有状态属性", + "tooltip.anvillib_rendering.structure_preview.block_entity": "方块实体数据(结构中保存的配置):", + "tooltip.anvillib_rendering.structure_preview.empty_block_entity": "未保存额外配置", + "tooltip.anvillib_rendering.structure_preview.data_limit": "数据过大,后续内容未展开", + "tooltip.anvillib_rendering.structure_preview.page": "%s/%s 页 · Shift+滚轮翻页", + "tooltip.anvillib_rendering.structure_preview.nbt.field.ExtraData": "配置", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Inventory": "库存", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Items": "物品", + "tooltip.anvillib_rendering.structure_preview.nbt.field.items": "物品", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Filter": "过滤器", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Filtering": "过滤设置", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterEnabled": "启用过滤", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterMode": "匹配模式", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Slot": "槽位", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Size": "大小", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotItem": "槽位物品", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterItem": "标记物品", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterEnabled": "启用槽位过滤", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotLimit": "槽位上限", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Disabled": "禁用", + "tooltip.anvillib_rendering.structure_preview.nbt.field.IsEmptySlot": "空槽位", + "tooltip.anvillib_rendering.structure_preview.nbt.field.id": "标识", + "tooltip.anvillib_rendering.structure_preview.nbt.field.count": "数量", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Count": "数量", + "tooltip.anvillib_rendering.structure_preview.nbt.field.components": "组件", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Range": "检测范围", + "tooltip.anvillib_rendering.structure_preview.nbt.field.RangeRadius": "范围半径", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Cooldown": "冷却", + "tooltip.anvillib_rendering.structure_preview.nbt.field.OutputMode": "输出模式", + "tooltip.anvillib_rendering.structure_preview.nbt.field.OutputSignal": "输出信号", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Tank": "储罐", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Fluid": "流体", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FluidName": "流体标识", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Amount": "数量", + "tooltip.anvillib_rendering.structure_preview.nbt.field.amount": "数量", + "tooltip.anvillib_rendering.structure_preview.nbt.field.marked_item": "标记物品", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Enhanced": "已增强", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Infinite": "无限", + "tooltip.anvillib_rendering.structure_preview.nbt.field.CustomName": "自定义名称", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Lock": "锁定", + "tooltip.anvillib_rendering.structure_preview.nbt.field.LootTable": "战利品表", + "tooltip.anvillib_rendering.structure_preview.nbt.field.LootTableSeed": "战利品种子", + "tooltip.anvillib_rendering.structure_preview.nbt.field.RecipesUsed": "已用配方", + "tooltip.anvillib_rendering.structure_preview.nbt.field.BurnTime": "燃烧时间", + "tooltip.anvillib_rendering.structure_preview.nbt.field.CookTime": "烹饪进度", + "tooltip.anvillib_rendering.structure_preview.nbt.field.CookTimeTotal": "烹饪总时长", + "tooltip.anvillib_rendering.structure_preview.nbt.field.TransferCooldown": "传输冷却", + "tooltip.anvillib_rendering.structure_preview.nbt.field.RecordItem": "唱片", + "tooltip.anvillib_rendering.structure_preview.nbt.field.front_text": "正面文字", + "tooltip.anvillib_rendering.structure_preview.nbt.field.back_text": "背面文字", + "tooltip.anvillib_rendering.structure_preview.nbt.field.messages": "文字", + "tooltip.anvillib_rendering.structure_preview.nbt.field.filtered_messages": "过滤后文字", + "tooltip.anvillib_rendering.structure_preview.nbt.field.color": "颜色", + "tooltip.anvillib_rendering.structure_preview.nbt.field.has_glowing_text": "发光文字", + "tooltip.anvillib_rendering.structure_preview.nbt.field.is_waxed": "已封蜡", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Patterns": "图案", + "tooltip.anvillib_rendering.structure_preview.nbt.field.patterns": "图案", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Base": "底色", + "tooltip.anvillib_rendering.structure_preview.nbt.field.BrewTime": "酿造时间", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Fuel": "燃料", + "tooltip.anvillib_rendering.structure_preview.nbt.field.EntityId": "实体标识", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SpawnData": "生成数据", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SpawnPotentials": "生成候选", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Delay": "延迟", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Command": "命令", + "tooltip.anvillib_rendering.structure_preview.nbt.field.LastOutput": "上次输出", + "tooltip.anvillib_rendering.structure_preview.nbt.field.auto": "自动", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Energy": "能量", + "tooltip.anvillib_rendering.structure_preview.nbt.field.InputPower": "输入功率", + "tooltip.anvillib_rendering.structure_preview.nbt.field.storedEnergy": "存储能量", + "tooltip.anvillib_rendering.structure_preview.nbt.field.laserLevel": "激光等级", + "tooltip.anvillib_rendering.structure_preview.nbt.field.lensType": "透镜类型", + "tooltip.anvillib_rendering.structure_preview.nbt.field.gamma": "伽马模式", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Uniform": "均匀模式", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Faces": "骰子面数", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Output": "输出信号", + "tooltip.anvillib_rendering.structure_preview.nbt.field.operation": "操作", + "tooltip.anvillib_rendering.structure_preview.nbt.field.target": "目标", + "tooltip.anvillib_rendering.structure_preview.nbt.field.placement": "放置", + "tooltip.anvillib_rendering.structure_preview.nbt.field.loadedStructureName": "结构名称", + "tooltip.anvillib_rendering.structure_preview.nbt.field.rangeX": "X 范围", + "tooltip.anvillib_rendering.structure_preview.nbt.field.rangeY": "Y 范围", + "tooltip.anvillib_rendering.structure_preview.nbt.field.rangeZ": "Z 范围", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterEnabled.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.FilterEnabled.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterEnabled.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.SlotFilterEnabled.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Disabled.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Disabled.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.IsEmptySlot.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.IsEmptySlot.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Enhanced.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Enhanced.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Infinite.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Infinite.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.has_glowing_text.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.has_glowing_text.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.is_waxed.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.is_waxed.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.gamma.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.gamma.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Uniform.value.0": "否", + "tooltip.anvillib_rendering.structure_preview.nbt.field.Uniform.value.1": "是", + "tooltip.anvillib_rendering.structure_preview.no_gui": "此方块没有可预览的界面" +} diff --git a/module.rendering/src/previewTest/java/dev/anvilcraft/lib/v2/rendering/preview/gui/PreviewRegressionTest.java b/module.rendering/src/previewTest/java/dev/anvilcraft/lib/v2/rendering/preview/gui/PreviewRegressionTest.java new file mode 100644 index 000000000..5b10f2ea7 --- /dev/null +++ b/module.rendering/src/previewTest/java/dev/anvilcraft/lib/v2/rendering/preview/gui/PreviewRegressionTest.java @@ -0,0 +1,55 @@ +package dev.anvilcraft.lib.v2.rendering.preview.gui; + +import net.minecraft.resources.ResourceLocation; + +/** 验证提供器选择以及只读绘制作用域在嵌套和异常时的状态恢复。 */ +public final class PreviewRegressionTest { + private PreviewRegressionTest() { + } + + public static void main(String[] args) { + var first = ResourceLocation.fromNamespaceAndPath("test", "preview_first"); + var second = ResourceLocation.fromNamespaceAndPath("test", "preview_second"); + StructureGuiProviders.Provider provider = context -> null; + require(StructureGuiProviders.get(first) == null, "未知方块应使用原生菜单路径"); + StructureGuiProviders.register(first, provider); + require(StructureGuiProviders.get(first) == provider && StructureGuiProviders.get(second) == null, + "提供器必须精确匹配方块标识"); + boolean rejected = false; + try { + StructureGuiProviders.register(first, context -> null); + } catch (IllegalStateException exception) { + rejected = true; + } + require(rejected && StructureGuiProviders.get(first) == provider, "重复注册不得覆盖已有提供器"); + require(!StructureGuiSession.isRendering(), "绘制作用域初始应关闭"); + StructureGuiSession.renderOnly(0.5f, () -> { + require(StructureGuiSession.isRendering(), "进入作用域后应启用只读绘制"); + require(StructureGuiSession.tint(0x80FFFFFF) == 0x40FFFFFF, "应保留原色并相乘透明度"); + try { + StructureGuiSession.renderOnly(0.25f, () -> { + require(StructureGuiSession.tint(0x80FFFFFF) == 0x20FFFFFF, "嵌套作用域应使用自己的透明度"); + throw new IllegalArgumentException("验证异常退出"); + }); + } catch (IllegalArgumentException exception) { + require(StructureGuiSession.isRendering() && StructureGuiSession.opacity() == 0.5f, + "异常退出后必须恢复外层作用域"); + } + }); + require(!StructureGuiSession.isRendering() && StructureGuiSession.tint(0x80FFFFFF) == 0x80FFFFFF, + "结束后不得影响普通界面绘制"); + float previous = StructureGuiGeometry.easeOut(0); + for (int index = 1; index <= 100; index++) { + float value = StructureGuiGeometry.easeOut(index / 100f); + require(value >= previous && value <= 1, "浮窗过渡应单调且不越界"); + previous = value; + } + System.out.println("共享结构预览回归检查通过"); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +}