Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions module.rendering/README.md
Original file line number Diff line number Diff line change
@@ -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` 执行。渲染画面和第三方容器仍需客户端验证。
29 changes: 29 additions & 0 deletions module.rendering/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') }
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Boolean> cir) {
if (StructureGuiSession.isRendering()) {
cir.setReturnValue(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@org.jspecify.annotations.NullMarked
package dev.anvilcraft.lib.v2.rendering.mixins.preview;
Original file line number Diff line number Diff line change
@@ -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_);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading
Loading