diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..f0a830a9b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ProjectRed is a NeoForge Minecraft mod (MC 1.21.1, NeoForge 21.1.72, Java 21) focused on Redstone circuitry. It uses a multi-module Gradle build. + +## Build Commands + +```bash +./gradlew build # Build all modules +./gradlew :core:build # Build a specific module +./gradlew genIntellijRuns # Generate IntelliJ run configs +./gradlew test # Run all unit tests +./gradlew :core:test # Run unit tests for a specific module +./gradlew :transmission:runGameTestServer # Run in-game tests for transmission module +``` + +Built JARs are placed in `/build/libs/`. + +## Module Architecture + +The project is split into 8 modules with a strict dependency hierarchy: + +``` +api (no mod deps — public API for other mods) +core → api, CodeChickenLib, CBMultipart, JEI, CCTweaked + ├── expansion (pistons, frames, block movement) + ├── exploration (world generation content) + ├── illumination (lighting blocks) + ├── integration (logic gates and basic circuits) + └── transmission (wire networks and signal routing) + └── fabrication → core + integration + transmission (factory automation, largest module) +runtime (empty module; aggregates all modules for IDE run configs) +``` + +Each module's main class is annotated with `@Mod("projectred_")`. + +### Key Internal Packages (core module) + +- `mrtjp.projectred.core` — Mod entry point (`ProjectRedCore.java`) +- `mrtjp.projectred.core.init` — Deferred registers (blocks, items, menus, parts, creative tabs) +- `mrtjp.projectred.core.tile` — Block entities +- `mrtjp.projectred.core.part` — CBMultipart multipart definitions +- `mrtjp.projectred.core.power` — Power/energy system interfaces +- `mrtjp.projectred.lib` — Shared utilities (VecLib, ModelLib, etc.) +- `mrtjp.projectred.redui` — Custom UI framework used across all modules + +### CBMultipart Integration + +Blocks that can coexist in the same world position (e.g., wires) are implemented as "parts" rather than standard blocks. These extend CBMultipart's `MultiPart` class and register via `CBMultipartPlugin`. Parts live in `*.part` packages. + +### Public API + +`mrtjp.projectred.api.ProjectRedAPI` is the entry point for other mods. Module API fields (e.g., `transmissionAPI`, `expansionAPI`) are nullable — they're only set when the module is loaded. Other mods should use soft dependencies. + +## Data Generation + +Generated assets (models, recipes, tags, lang files) are output to `/src/main/generated/`. Do not manually edit files in `generated/` — they are regenerated by datagen runs (`./gradlew ::runData`). + +Translation files go in `/src/main/resources/assets/projectred_/lang/`. Run the verification script after editing translations: + +```bash +python3 .github/verify_lang_files.py --language --verbose +python3 .github/verify_lang_files.py --language --fix # auto-fix ordering/missing keys +``` + +## Testing + +- **Unit tests** (JUnit 5): `/src/test/java` — currently in `core` (VecLib, ModelLib, voxel shapes) +- **Game tests** (NeoForge GameTest framework): `transmission` module has active game test infrastructure; test namespace is `projectred_transmission` + +## CI + +Pull requests run: commitlint, lang file verification, `./gradlew build`, and transmission game tests. Commit messages must follow conventional commits (see `.github/commitlint.config.js`). \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3b1035547..0af1137de 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ subprojects { p -> maven { url = "https://maven.covers1624.net/" } maven { url = "https://maven.squiddev.cc" } maven { url = "https://maven.blamejared.com/" } + maven { url = "https://api.modrinth.com/maven" } } // Replace version tokens in mods.toml @@ -58,7 +59,8 @@ subprojects { p -> 'lang_version': forge_version.split('\\.')[0], 'ccl_version': ccl_version, 'cbm_version': cbm_version, - 'cct_version': cct_version + 'cct_version': cct_version, + 'embeddium_version': embeddium_version, ] inputs.properties(expandMap) diff --git a/core/src/main/java/mrtjp/projectred/core/client/FullyOrientableBlockModel.java b/core/src/main/java/mrtjp/projectred/core/client/FullyOrientableBlockModel.java new file mode 100644 index 000000000..d709b8e37 --- /dev/null +++ b/core/src/main/java/mrtjp/projectred/core/client/FullyOrientableBlockModel.java @@ -0,0 +1,134 @@ +package mrtjp.projectred.core.client; + +import codechicken.lib.model.PerspectiveModel; +import codechicken.lib.model.PerspectiveModelState; +import codechicken.lib.render.CCModel; +import codechicken.lib.render.CCRenderState; +import codechicken.lib.render.buffer.BakedQuadVertexBuilder; +import codechicken.lib.util.TransformUtils; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Rotation; +import codechicken.lib.vec.Vector3; +import codechicken.lib.vec.uv.MultiIconTransformation; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.ItemOverrides; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.client.ChunkRenderTypeSet; +import net.neoforged.neoforge.client.model.IDynamicBakedModel; +import net.neoforged.neoforge.client.model.data.ModelData; +import net.neoforged.neoforge.client.model.generators.ConfiguredModel; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * Needs to exist because Vanilla's default {@link ConfiguredModel} only supports a single x rotation and y rotation, + * making it very difficult to render blocks that can not only be oriented to all 6 sides, but also rotated about those + * sides. With only x/y rotations, we would have to use UV-mapping to rotate the textures themselves rather than the model. + */ +public abstract class FullyOrientableBlockModel implements IDynamicBakedModel, PerspectiveModel { + // State -> Side -> Quads list + private final HashMap>> modelMap = new HashMap<>(); + + //region Implementation abstracts + protected abstract RenderType getBlockRenderLayer(@Nullable BlockState state); + + protected abstract RenderData getBlockRenderData(@Nullable BlockState state); + + protected abstract BlockState getItemRenderState(); + //endregion + + @Override + public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData date, @Nullable RenderType renderType) { + // Render type must match + if (renderType != null && renderType != getBlockRenderLayer(state)) { + return List.of(); + } + // Full block, so no uncullable sides + if (side == null) { + return List.of(); + } + + if (state == null) { + state = getItemRenderState(); + } + + return getOrGenerateQuads(state, side); + } + + private List getOrGenerateQuads(BlockState state, Direction side) { + HashMap> sideMap = modelMap.get(state); + if (sideMap != null) return sideMap.get(side.ordinal()); + + synchronized (modelMap) { + // Re-check after waiting for sync + sideMap = modelMap.get(state); + if (sideMap == null) { + sideMap = generateSideMap(state); + modelMap.put(state, sideMap); + } + } + + return sideMap.get(side.ordinal()); + } + + private HashMap> generateSideMap(BlockState state) { + // Prep render + CCRenderState ccrs = CCRenderState.instance(); + ccrs.reset(); + ccrs.computeLighting = false; + ccrs.brightness = 0; + BakedQuadVertexBuilder builder = new BakedQuadVertexBuilder(); + ccrs.bind(builder, DefaultVertexFormat.BLOCK); + + // Render full block model with orient transform + RenderData data = getBlockRenderData(state); + CCModel m = CCModel.quadModel(24) + .generateBlock(0, Cuboid6.full, 0) + .apply(Rotation.sideOrientation(data.side, data.rotation).at(Vector3.CENTER)) + .computeNormals() + .shrinkUVs(0.0005); + + m.render(ccrs, data.iconT); + + // Bake and separate sides + HashMap> sideMap = new HashMap<>(); + List blockQuads = builder.bake(); + for (int s = 0; s < 6; s++) { + LinkedList sideQuads = new LinkedList<>(); + for (BakedQuad quad : blockQuads) { + if (quad.getDirection().ordinal() == s) { + sideQuads.add(quad); + } + } + sideMap.put(s, sideQuads); + } + + return sideMap; + } + + @Override + public ChunkRenderTypeSet getRenderTypes(BlockState state, RandomSource rand, ModelData data) { + return ChunkRenderTypeSet.of(RenderType.solid()); + } + + public record RenderData(int side, int rotation, MultiIconTransformation iconT) { + } + + //region BakedModel + //@formatter:off + @Override public boolean useAmbientOcclusion() { return true; } + @Override public boolean isGui3d() { return true; } + @Override public boolean usesBlockLight() { return true; } + @Override public boolean isCustomRenderer() { return false; } + @Override public ItemOverrides getOverrides() { return ItemOverrides.EMPTY; } + @Override public @Nullable PerspectiveModelState getModelState() { return TransformUtils.DEFAULT_BLOCK; } + //@formatter:on + //endregion +} diff --git a/core/src/main/java/mrtjp/projectred/core/client/FullyOrientableBlockRenderer.java b/core/src/main/java/mrtjp/projectred/core/client/FullyOrientableBlockRenderer.java deleted file mode 100644 index 5932f2681..000000000 --- a/core/src/main/java/mrtjp/projectred/core/client/FullyOrientableBlockRenderer.java +++ /dev/null @@ -1,107 +0,0 @@ -package mrtjp.projectred.core.client; - -import codechicken.lib.render.CCModel; -import codechicken.lib.render.CCRenderState; -import codechicken.lib.render.block.ICCBlockRenderer; -import codechicken.lib.render.buffer.TransformingVertexConsumer; -import codechicken.lib.render.item.IItemRenderer; -import codechicken.lib.vec.Cuboid6; -import codechicken.lib.vec.Rotation; -import codechicken.lib.vec.Vector3; -import codechicken.lib.vec.uv.MultiIconTransformation; -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import net.minecraft.client.renderer.ItemBlockRenderTypes; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.core.BlockPos; -import net.minecraft.util.RandomSource; -import net.minecraft.world.item.ItemDisplayContext; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.BlockAndTintGetter; -import net.minecraft.world.level.block.state.BlockState; -import net.neoforged.neoforge.client.model.data.ModelData; -import net.neoforged.neoforge.client.model.generators.ConfiguredModel; - -import javax.annotation.Nullable; - -/** - * Needs to exist because Vanilla's default {@link ConfiguredModel} only supports a single x rotation and y rotation, - * making it very difficult to render blocks that can not only be oriented to all 6 sides, but also rotated about those - * sides. With only x/y rotations, we would have to use UV-mapping to rotate the textures themselves rather than the model. - */ -public abstract class FullyOrientableBlockRenderer implements ICCBlockRenderer, IItemRenderer { - - private static final CCModel[] models = new CCModel[6 * 4]; - - static { - CCModel baseModel = CCModel.quadModel(24).generateBlock(0, Cuboid6.full); - for (int s = 0; s < 6; s++) { - for (int r = 0; r < 4; r++) { - CCModel m = baseModel.copy().apply(Rotation.sideOrientation(s, r).at(Vector3.CENTER)); - m.computeNormals(); - m.shrinkUVs(0.0005); - m.computeLightCoords(); - models[modelKey(s, r)] = m; - } - } - } - - private static int modelKey(int side, int rot) { - return side << 2 | rot & 3; - } - - //region Implementation abstracts - protected abstract RenderType getBlockRenderLayer(BlockState state, BlockPos pos, BlockAndTintGetter level); - - protected abstract RenderData getBlockRenderData(BlockState state, BlockPos pos, BlockAndTintGetter level); - - protected abstract RenderData getItemRenderData(ItemStack stack); - //endregion - - @Override - public void renderBlock(BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack mStack, VertexConsumer builder, RandomSource random, ModelData data, @Nullable RenderType renderType) { - CCRenderState ccrs = CCRenderState.instance(); - ccrs.reset(); - ccrs.bind(new TransformingVertexConsumer(builder, mStack), DefaultVertexFormat.BLOCK); - ccrs.lightMatrix.locate(world, pos); - - render(ccrs, renderType, getBlockRenderData(state, pos, world)); - } - - @Override - public void renderItem(ItemStack stack, ItemDisplayContext transformType, PoseStack mStack, MultiBufferSource source, int packedLight, int packedOverlay) { - - CCRenderState ccrs = CCRenderState.instance(); - ccrs.reset(); - ccrs.brightness = packedLight; - ccrs.overlay = packedOverlay; - ccrs.bind(ItemBlockRenderTypes.getRenderType(stack, true), source, mStack); - - render(ccrs, null, getItemRenderData(stack)); - } - - private void render(CCRenderState ccrs, @Nullable RenderType layer, RenderData data) { - - CCModel model = models[modelKey(data.side, data.rotation)]; - MultiIconTransformation iconT = data.iconT; - - if (layer != null) { - model.render(ccrs, iconT, ccrs.lightMatrix); - } else { - model.render(ccrs, iconT); - } - } - - public record RenderData(int side, int rotation, MultiIconTransformation iconT) { - } - - // Note: Due to an obfuscation bug, these must be manually implemented in the subclass -// //@formatter:off -// @Override public boolean useAmbientOcclusion() { return true; } -// @Override public boolean isGui3d() { return true; } -// @Override public boolean usesBlockLight() { return true; } -// @Override public @Nullable PerspectiveModelState getModelState() { return TransformUtils.DEFAULT_BLOCK; } -// //@formatter:on -} diff --git a/expansion/build.gradle b/expansion/build.gradle index 87c8facff..95725405f 100644 --- a/expansion/build.gradle +++ b/expansion/build.gradle @@ -38,6 +38,9 @@ dependencies { compileOnly project(":api") implementation project(":core") + compileOnly "maven.modrinth:embeddium:${embeddium_version}" + compileOnly "maven.modrinth:sodium:${sodium_version}" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' testImplementation 'org.mockito:mockito-core:5.14.2' diff --git a/expansion/src/main/generated/.cache/b71c44817acda374fc60c60d51d7f5eec540e17e b/expansion/src/main/generated/.cache/b71c44817acda374fc60c60d51d7f5eec540e17e index 168257c2d..f1fd6f11e 100644 --- a/expansion/src/main/generated/.cache/b71c44817acda374fc60c60d51d7f5eec540e17e +++ b/expansion/src/main/generated/.cache/b71c44817acda374fc60c60d51d7f5eec540e17e @@ -1,13 +1,13 @@ -// 1.21.1 2025-08-10T20:38:13.26732 Block States: projectred_expansion +// 1.21.1 2026-04-12T17:22:00.453012 Block States: projectred_expansion b697bf3bc54738e649092c6b88cbb4fd9b3da8a3 assets/projectred_expansion/blockstates/auto_crafter.json 1d92e2503aa76e8cf9e908f7afa248a291fcf387 assets/projectred_expansion/blockstates/battery_box.json 325a4ac57dbf6f6f049812d1442992b5f7368d21 assets/projectred_expansion/blockstates/block_breaker.json ac22e2b26c326b40d94951ed7a69b7b994f14aca assets/projectred_expansion/blockstates/charging_bench.json 2742c5200239a1933b73b45885d8d5fe0ec5ed3d assets/projectred_expansion/blockstates/deployer.json fe88677fa2e1cf270a91e8e1ac1f8b598786949f assets/projectred_expansion/blockstates/fire_starter.json -e285658b615ec1141d09ddb523656ad8dcd0c634 assets/projectred_expansion/blockstates/frame.json +ca6b372af9ea72459f49dc7ffd30ce5c744aedf9 assets/projectred_expansion/blockstates/frame.json 2ea35b8dcec2486da65f2464436c04aba36624b1 assets/projectred_expansion/blockstates/frame_actuator.json -da1694043a21b0b85aa2918f534786f5e0de0cb7 assets/projectred_expansion/blockstates/frame_motor.json +906c41f10706c0c5d48631877a7fa58a18590b8b assets/projectred_expansion/blockstates/frame_motor.json 97acab0b18004d35a03674349f4c14542707ad66 assets/projectred_expansion/blockstates/project_bench.json 99538255fa8dddf3c50e280e1e4d9f7d3248231c assets/projectred_expansion/blockstates/transposer.json 37c0bb69db6a7bdf44866469f4d8e0a69d22d137 assets/projectred_expansion/models/block/auto_crafter.json @@ -30,11 +30,11 @@ b2c9b5a1a4bf62a48df2c407c571f58251dd79ea assets/projectred_expansion/models/bloc 8abdb147b3c3e33a1c3ca5903f6bf57b269061c8 assets/projectred_expansion/models/block/deployer_active.json 99b61408e0d2bdaae3a09a6c17e762435068537e assets/projectred_expansion/models/block/fire_starter.json d34961059b05c1caf2059ffba539b5ffc38c36a5 assets/projectred_expansion/models/block/fire_starter_active.json +9049814fa7507a8bfdb47497310140da703d6417 assets/projectred_expansion/models/block/frame.json 9dbba6f59658244da05c4cdeaeb1691deef902d8 assets/projectred_expansion/models/block/frame_actuator.json 9130fd3156286ec9bcc1e958be94505d47cf1e36 assets/projectred_expansion/models/block/frame_actuator_state1.json 441616b7dbe0c0ec337ad807c7214514c45979dc assets/projectred_expansion/models/block/frame_actuator_state2.json -176677c516b3203abbf46daed70cbcfcca83eab5 assets/projectred_expansion/models/block/frame_motor_programmatically_rendered.json -cf74edee7186e05fd24bd7fae56c2e23d1bd1de4 assets/projectred_expansion/models/block/frame_programmatically_rendered.json +9f4fe5a1992cc2f32eafc351e05c8c1f836e7e60 assets/projectred_expansion/models/block/frame_motor.json 645544cf14d5ca7f1577a11532a3321c06a51e18 assets/projectred_expansion/models/block/project_bench.json 17e1cc736c14625f137dbe9d5871e07d4eb85e73 assets/projectred_expansion/models/block/transposer.json 6bbf26b37dd2a946ced5c52327c3e4c860918566 assets/projectred_expansion/models/block/transposer_active.json diff --git a/expansion/src/main/generated/.cache/e9b9ccd445df9bc8a5c2450219d2603d62d31dd5 b/expansion/src/main/generated/.cache/e9b9ccd445df9bc8a5c2450219d2603d62d31dd5 index 48dade808..b8a0b20ee 100644 --- a/expansion/src/main/generated/.cache/e9b9ccd445df9bc8a5c2450219d2603d62d31dd5 +++ b/expansion/src/main/generated/.cache/e9b9ccd445df9bc8a5c2450219d2603d62d31dd5 @@ -1,4 +1,4 @@ -// 1.21.1 2025-08-10T20:38:13.265665 projectred_expansion Item models. +// 1.21.1 2026-04-12T17:10:13.738951 projectred_expansion Item models. 599e9047cbf1215359e3106b1a6a0757a569ffc9 assets/projectred_expansion/models/item/auto_crafter.json 736ab07c8745c163e8a3cb90abc9308466032009 assets/projectred_expansion/models/item/battery.json d887cd7b776f1227da04cf267bf0a48dbc706027 assets/projectred_expansion/models/item/battery_box.json @@ -16,9 +16,9 @@ ca8ee7426c8d4b145e75bc0bebca77c90e05779e assets/projectred_expansion/models/item f8ad62a7aa4f5ae87d36e422722c4ffbf46d22e1 assets/projectred_expansion/models/item/electric_screwdriver.json c04ad8c2f44fd4f5a6ea7c5bb9ddf98629332a84 assets/projectred_expansion/models/item/empty_battery.json 8adfe244bf33706f67d587942e9179b74cf38fd0 assets/projectred_expansion/models/item/fire_starter.json -432462cb1b080b869c472c0e8f7495e9b4e1d638 assets/projectred_expansion/models/item/frame.json +4b23c55840a25bba29226122efb428357ce7c98b assets/projectred_expansion/models/item/frame.json 933ec48f28e2627aefbdcaa7d989eeac271ad282 assets/projectred_expansion/models/item/frame_actuator.json -78ea8ea37f9467201dc2e05ef372f4403ccbadb2 assets/projectred_expansion/models/item/frame_motor.json +369295af47e837e0fffaf9174fda04d7175bbc4d assets/projectred_expansion/models/item/frame_motor.json efabd61ed85b4d67f546ea6cfb90d9f5f17773cc assets/projectred_expansion/models/item/pneumatic_tube.json 9457da7d0b200c883ca98241d9bf155043221c53 assets/projectred_expansion/models/item/project_bench.json 5d8ae0bcb189be1eb7ce726fc4d28637dcdfaac8 assets/projectred_expansion/models/item/recipe_plan.json diff --git a/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame.json b/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame.json index 5164d0d7e..c94758858 100644 --- a/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame.json +++ b/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame.json @@ -1,7 +1,7 @@ { "variants": { "": { - "model": "projectred_expansion:block/frame_programmatically_rendered" + "model": "projectred_expansion:block/frame" } } } \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame_motor.json b/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame_motor.json index ca40c7631..e5164c809 100644 --- a/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame_motor.json +++ b/expansion/src/main/generated/assets/projectred_expansion/blockstates/frame_motor.json @@ -1,7 +1,7 @@ { "variants": { "": { - "model": "projectred_expansion:block/frame_motor_programmatically_rendered" + "model": "projectred_expansion:block/frame_motor" } } } \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/models/block/frame.json b/expansion/src/main/generated/assets/projectred_expansion/models/block/frame.json new file mode 100644 index 000000000..9fb743837 --- /dev/null +++ b/expansion/src/main/generated/assets/projectred_expansion/models/block/frame.json @@ -0,0 +1,5 @@ +{ + "parent": "minecraft:block/block", + "class": "mrtjp.projectred.expansion.client.FrameBlockModel", + "loader": "codechickenlib:class" +} \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_motor.json b/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_motor.json new file mode 100644 index 000000000..983bb2575 --- /dev/null +++ b/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_motor.json @@ -0,0 +1,5 @@ +{ + "parent": "minecraft:block/block", + "class": "mrtjp.projectred.expansion.client.FrameMotorBlockModel", + "loader": "codechickenlib:class" +} \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_motor_programmatically_rendered.json b/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_motor_programmatically_rendered.json deleted file mode 100644 index 198e4d0da..000000000 --- a/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_motor_programmatically_rendered.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "minecraft:block/block", - "textures": { - "particle": "projectred_expansion:block/frame_motor_top" - } -} \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_programmatically_rendered.json b/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_programmatically_rendered.json deleted file mode 100644 index 1c699b71e..000000000 --- a/expansion/src/main/generated/assets/projectred_expansion/models/block/frame_programmatically_rendered.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "minecraft:block/block", - "textures": { - "particle": "projectred_expansion:block/frame" - } -} \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/models/item/frame.json b/expansion/src/main/generated/assets/projectred_expansion/models/item/frame.json index 257f8df06..a4f824715 100644 --- a/expansion/src/main/generated/assets/projectred_expansion/models/item/frame.json +++ b/expansion/src/main/generated/assets/projectred_expansion/models/item/frame.json @@ -1,5 +1,3 @@ { - "parent": "minecraft:item/generated", - "class": "mrtjp.projectred.expansion.client.FrameBlockRenderer", - "loader": "codechickenlib:class" + "parent": "projectred_expansion:block/frame" } \ No newline at end of file diff --git a/expansion/src/main/generated/assets/projectred_expansion/models/item/frame_motor.json b/expansion/src/main/generated/assets/projectred_expansion/models/item/frame_motor.json index 1d3d1288f..2b58f8d4c 100644 --- a/expansion/src/main/generated/assets/projectred_expansion/models/item/frame_motor.json +++ b/expansion/src/main/generated/assets/projectred_expansion/models/item/frame_motor.json @@ -1,5 +1,3 @@ { - "parent": "minecraft:item/generated", - "class": "mrtjp.projectred.expansion.client.FrameMotorBlockRenderer", - "loader": "codechickenlib:class" + "parent": "projectred_expansion:block/frame_motor" } \ No newline at end of file diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/MovementManager.java b/expansion/src/main/java/mrtjp/projectred/expansion/MovementManager.java index 94846b0ee..286f23f4f 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/MovementManager.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/MovementManager.java @@ -1,37 +1,16 @@ package mrtjp.projectred.expansion; import codechicken.lib.data.MCDataInput; -import codechicken.lib.data.MCDataOutput; import codechicken.lib.packet.PacketCustom; -import codechicken.lib.vec.Vector3; -import com.mojang.blaze3d.vertex.PoseStack; -import mrtjp.projectred.api.BlockMover; -import mrtjp.projectred.api.MovementController; import mrtjp.projectred.api.MovementDescriptor; import mrtjp.projectred.core.Configurator; -import mrtjp.projectred.expansion.client.MovingBlockSuppressorRenderer; -import mrtjp.projectred.lib.VecLib; -import net.covers1624.quack.collection.FastStream; -import net.covers1624.quack.util.LazyValue; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.ItemBlockRenderTypes; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; import net.minecraft.core.RegistryAccess; -import net.minecraft.core.SectionPos; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.util.RandomSource; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.Vec3; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.api.distmarker.OnlyIn; -import net.neoforged.neoforge.client.event.RenderLevelStageEvent; import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.level.ChunkWatchEvent; import net.neoforged.neoforge.event.level.LevelEvent; @@ -39,9 +18,10 @@ import javax.annotation.Nullable; import java.util.*; -import java.util.function.Consumer; +import java.util.concurrent.ConcurrentHashMap; -import static mrtjp.projectred.api.MovementDescriptor.MovementStatus.*; +import static mrtjp.projectred.api.MovementDescriptor.MovementStatus.MOVING; +import static mrtjp.projectred.api.MovementDescriptor.MovementStatus.PENDING_FINALIZATION; import static mrtjp.projectred.expansion.ProjectRedExpansion.LOGGER; public class MovementManager { @@ -55,7 +35,7 @@ public class MovementManager { private static final int KEY_CANCEL_MOVE = 0x3; private final ResourceKey dimension; - private final Map structures = new HashMap<>(); + private final Map structures = new ConcurrentHashMap<>(); private final HashMap> watchingPlayers = new HashMap<>(); private final HashMap> newWatchers = new HashMap<>(); @@ -74,6 +54,11 @@ public static MovementManager getInstance(Level level) { return CLIENT_INSTANCE.get(clientLevel.dimension()); } + public static @Nullable MovementManager getInstanceNullable(Level level) { + var map = level.isClientSide() ? CLIENT_INSTANCE : SERVER_INSTANCE; + return map.get(level.dimension()); + } + public MovementManager(ResourceKey dimension) { this.dimension = dimension; LOGGER.debug("Created MovementManager for dimension {}", dimension.location()); @@ -119,72 +104,6 @@ public static void onLevelTick(LevelTickEvent.Post event) { getInstance(event.getLevel()).tick(event.getLevel()); } - @OnlyIn(Dist.CLIENT) - public static void onRenderLevelStage(RenderLevelStageEvent event) { - //TODO move to separate class - - Level level = Minecraft.getInstance().level; - if (level == null) return; - - MovementManager manager = getInstance(level); - if (manager.structures.isEmpty()) return; - - // Get the renderType for this stage, and skip if we dont care about it - List renderTypes = List.of(RenderType.solid(), RenderType.cutout(), RenderType.cutoutMipped(), RenderType.translucent()); - RenderType renderType = null; - for (RenderType type : renderTypes) { - if (RenderLevelStageEvent.Stage.fromRenderType(type) == event.getStage()) { - renderType = type; - break; - } - } - if (renderType == null) return; - - RandomSource random = RandomSource.create(); - - // Set up camera pose - Vec3 cam = event.getCamera().getPosition(); - PoseStack stack = event.getPoseStack(); - stack.pushPose(); - stack.mulPose(event.getModelViewMatrix()); - stack.translate(-cam.x, -cam.y, -cam.z); - - for (MovingStructure structure : manager.structures.values()) { - - // Set up render offset based on progress of movement - Vector3 offset = structure.getRenderOffset(event.getPartialTick().getGameTimeDeltaPartialTick(false)); - stack.pushPose(); - stack.translate(offset.x, offset.y, offset.z); - - MultiBufferSource.BufferSource buffers = Minecraft.getInstance().renderBuffers().bufferSource(); - - for (MovingRow row : structure.rows) { - Iterator it = row.iteratePreMove(); - while (it.hasNext()) { - BlockPos p = it.next(); - BlockState state = level.getBlockState(p); - - if (!ItemBlockRenderTypes.getRenderLayers(state).contains(renderType)) continue; - - // Render the moving block - stack.pushPose(); - stack.translate(p.getX(), p.getY(), p.getZ()); - - MovingBlockSuppressorRenderer.allowMovingRenderOnRenderThread = true; - Minecraft.getInstance().getBlockRenderer().renderBatched(state, p, level, stack, buffers.getBuffer(renderType), false, random, level.getModelData(p), renderType); - MovingBlockSuppressorRenderer.allowMovingRenderOnRenderThread = false; - - stack.popPose(); //p - } - } - - buffers.endBatch(); - stack.popPose(); //offset - } - - stack.popPose(); //cam - } - private void addChunkWatcher(ChunkPos pos, ServerPlayer player) { // LOGGER.debug("Player {} started watching chunk {},{} (isClient: {})", player.getName().getString(), pos.x, pos.z, player.level.isClientSide); newWatchers.computeIfAbsent(player, p -> new HashSet<>()).add(pos); @@ -278,19 +197,10 @@ private void tick(Level level) { public MovementDescriptor beginMove(Level level, Set blocks, int dir, double speed) { if (blocks.size() > Configurator.SERVER.frameMoveLimit.get()) { - return InternalMovementInfo.failedMovement(blocks.size()); - } - - // Split set of blocks into rows going the opposite direction of the move - Set> rows = VecLib.resolveRows(blocks, dir^1); - - // Create MovingRows - List movingRows = new ArrayList<>(rows.size()); - for (List row : rows) { - movingRows.add(new MovingRow(row, dir)); + return MovingStructureInfo.failedMovement(blocks.size()); } - MovingStructure structure = new MovingStructure(getNextStructureId(), speed, dir, movingRows); + MovingStructure structure = MovingStructure.fromBlockSet(getNextStructureId(), speed, dir, blocks); if (!structure.canMove(level)) return structure; // Add structure and send to client @@ -307,11 +217,15 @@ public boolean hasNoMovingStructures() { return structures.isEmpty(); } - public InternalMovementInfo getMovementInfo(BlockPos pos) { + public Collection getMovingStructures() { + return structures.values(); + } + + public MovingStructureInfo getMovementInfo(BlockPos pos) { for (MovingStructure structure : structures.values()) { if (structure.contains(pos)) return structure; } - return InternalMovementInfo.NO_MOVEMENT_INFO; + return MovingStructureInfo.NO_MOVEMENT_INFO; } //region Network @@ -364,6 +278,19 @@ private void readStructureExecution(MCDataInput input, Level level) { return; } + assert structure.getStatus() == MOVING || structure.getStatus() == PENDING_FINALIZATION; + + // The client is usually behind by some ticks. Tick progress rapidly to complete the move. + // TODO Add tickProgressToEnd() method. Pushing entities is more efficient if done all at + int ticksBehind = 0; + while (structure.getStatus() == MOVING) { + structure.tickProgress(level); + ticksBehind++; + } + if (ticksBehind > 1) { + LOGGER.warn("Client structure with id {} was {} ticks behind!", id, ticksBehind); + } + // Execute pre-move and post-move operations. Server has only done pre-move so far. // It will do post-move after the client. structure.executePreMove(level); @@ -457,416 +384,4 @@ private Collection playersWatchingStructure(MovingStructure struct } //endregion - private static class MovingStructure implements InternalMovementInfo { - public final int id; - - private final double speed; - private final int dir; - private final List rows; - private final int totalSize; - - private final LazyValue> intersectingChunks = new LazyValue<>(this::computeIntersectingChunks); - private final LazyValue> renderChunks = new LazyValue<>(this::computeRenderChunks); - - private MovementStatus status; - private double progress; - - public MovingStructure(int id, double speed, int dir, List rows, MovementStatus status, double progress) { - this.id = id; - this.speed = speed; - this.dir = dir; - this.rows = Collections.unmodifiableList(rows); - this.status = status; - this.progress = progress; - this.totalSize = FastStream.of(rows).intSum(r -> r.size); - } - - public MovingStructure(int id, double speed, int dir, List rows) { - this(id, speed, dir, rows, PENDING_START, 0D); - } - - //region Network - public void writeDesc(MCDataOutput output) { - output.writeShort(id); - output.writeDouble(speed); - output.writeByte(dir); - output.writeShort(rows.size()); - for (MovingRow row : rows) { - output.writePos(row.pos); - output.writeShort(row.size); - } - output.writeByte(status.ordinal()); - output.writeDouble(progress); //TODO use integers instead - } - - public static MovingStructure fromDesc(MCDataInput input) { - int id = input.readUShort(); - double speed = input.readDouble(); - int dir = input.readUByte(); - int size = input.readUShort(); - List rows = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - rows.add(new MovingRow(input.readPos(), dir, input.readUShort())); - } - MovementStatus status = MovementStatus.values()[input.readUByte()]; - double progress = input.readDouble(); - - return new MovingStructure(id, speed, dir, rows, status, progress); - } - //endregion - - //region Movement description - @Override - public MovementStatus getStatus() { - return status; - } - - @Override - public boolean isMoving() { - return getStatus() == MOVING || getStatus() == PENDING_FINALIZATION; - } - - @Override - public double getProgress() { - return progress; - } - - @Override - public int getSize() { - return totalSize; - } - - @Override - public Vector3 getRenderOffset(float partialTicks) { - double p = Math.min(progress + speed * partialTicks, 1D); - return Vector3.fromBlockPos(BlockPos.ZERO.relative(Direction.values()[dir])).multiply(p); - } - //endregion - - public HashSet getChunkSet() { - return intersectingChunks.get(); - } - - public boolean intersects(ChunkPos pos) { - return intersectingChunks.get().contains(pos); - } - - public boolean contains(BlockPos pos) { - for (MovingRow row : rows) { - if (row.contains(pos)) return true; - } - return false; - } - - public void tickProgress(Level level) { - - // Should not be ticking progress otherwise - assert status == MOVING || status == PENDING_FINALIZATION; - - if (status == MOVING) { - progress = Math.min(progress + speed, 1D); - FastStream.of(rows).forEach(r -> r.pushEntities(level, progress)); - - if (progress >= 1D) { - status = PENDING_FINALIZATION; - } - } - } - - public boolean canMove(Level level) { - for (MovingRow row : rows) { - if (!row.canMove(level)) return false; - } - return true; - } - - public void beginMove(Level level) { - assert status == MovementStatus.PENDING_START; - status = MOVING; - - FastStream.of(rows).forEach(r -> r.beginMove(level)); - - if (level.isClientSide) { - // Force chunk to re-render so rendering of moving block can be suppressed by MovingBlockSuppressorRenderer - markChunksForRender(); - } - } - - public void executePreMove(Level level) { - // Silently moves blocks to new position - FastStream.of(rows).forEach(r -> r.moveBlocks(level)); - } - - public void executePostMove(Level level) { - // Completes the movement by alerting the tile itself, etc - FastStream.of(rows).forEach(r -> r.postMove(level)); - FastStream.of(rows).forEach(r -> r.endMove(level)); - - // Update neighbors - Set changes = new HashSet<>(); - FastStream.of(rows).forEach(r -> r.addNeighborChanges(level, changes)); - - for (BlockPos pos : changes) { - BlockState state = level.getBlockState(pos); - state.updateNeighbourShapes(level, pos, 0, 0); - state.updateIndirectNeighbourShapes(level, pos, 0, 0); - level.neighborChanged(pos, Blocks.AIR, pos); //TODO use better context here - } - - // Update lighting - markBlocksForLightUpdate(level); - - // Update chunk rendering - if (level.isClientSide) { - markChunksForRender(); - } - - // Mark chunks as changed - for (ChunkPos p : getChunkSet()) { - level.getChunk(p.x, p.z).setUnsaved(true); - } - - //TODO Tick rescheduling - status = FINISHED; - } - - public void cancelMove(Level level) { - // Shouldn't need to do anything. Nothing happens until the animation is finished - // TODO MovementController notification for this? - assert status == MOVING || status == PENDING_FINALIZATION; - status = CANCELLED; - } - - @OnlyIn(Dist.CLIENT) - private void markChunksForRender() { - FastStream.of(renderChunks.get()).forEach(p -> Minecraft.getInstance().levelRenderer.setSectionDirty(p.x(), p.y(), p.z(), true)); - } - - private void markBlocksForLightUpdate(Level level) { - FastStream.of(rows).forEach(r -> r.forEachAll(p -> level.getLightEngine().checkBlock(p))); - } - - private HashSet computeIntersectingChunks() { - HashSet chunks = new HashSet<>(); - FastStream.of(rows).forEach(r -> r.forEachAll(p -> chunks.add(new ChunkPos(p)))); - return chunks; - } - - private HashSet computeRenderChunks() { - HashSet chunks = new HashSet<>(); - FastStream.of(rows).forEach(r -> r.forEachAll(p -> { - // Add all neighbors of blocks as well to update culled faces - for (int s = 0; s < 6; s++) { - chunks.add(SectionPos.of(p.relative(Direction.values()[s]))); - } - // Note: no need to add position itself, as it *must* be in one of above chunks - })); - return chunks; - } - - @Override - public String toString() { - return "MovingStructure{" + - "id=" + id + - ", speed=" + speed + - ", dir=" + dir + - ", progress=" + progress + - ", rows=" + rows + - '}'; - } - } - - private static final class MovingRow { - - public final BlockPos pos; - public final int dir; - public final int size; - - private MovingRow(BlockPos pos, int dir, int size) { - this.pos = pos; - this.dir = dir; - this.size = size; - } - - private MovingRow(List row, int dir) { - // Row's head should be the next block towards dir where everything will move, - // then followed by the rest of the row - this.pos = row.get(0).relative(Direction.values()[dir]); - this.dir = dir; - this.size = row.size() + 1; - } - - public boolean contains(BlockPos pos) { - - BlockPos p1 = VecLib.projectDir(this.pos, dir); - BlockPos p2 = VecLib.projectDir(pos, dir); - - // If projections towards dir plane are not equal, they cannot be on same axis - if (!p1.equals(p2)) return false; - - // pos is on the same line as this row. Check if its between start and end - int a1 = VecLib.rejectComponent(this.pos, dir); - int a2 = VecLib.rejectComponent(this.pos.relative(Direction.values()[dir ^ 1], size - 1), dir); - int b = VecLib.rejectComponent(pos, dir); - - return Math.min(a1, a2) <= b && b <= Math.max(a1, a2); - } - - public boolean canMove(Level level) { - if (!level.isLoaded(pos)) return false; - BlockState state = level.getBlockState(pos); - if (!(state.isAir() || state.canBeReplaced())) return false; - - Iterator it = iteratePreMove(); - while (it.hasNext()) { - BlockPos pos = it.next(); - - BlockMover mover = MovementRegistry.getMover(level, pos); - if (!mover.canMove(level, pos)) return false; - - MovementController controller = MovementRegistry.getMovementController(level, pos); - // Allow hooks to conditionally block movement - if (controller != null && !controller.isMovable(level, pos, Direction.values()[dir])) return false; - } - - return true; - } - - public void beginMove(Level level) { - //TODO spawn movement blocks - - if (!level.isClientSide) { - // Notify blocks/BEs conforming to MovementController about move - forEachPreMove(p -> { - MovementController controller = MovementRegistry.getMovementController(level, p); - if (controller != null) controller.onMovementStarted(level, p, Direction.values()[dir]); - }); - } - } - - public void pushEntities(Level level, double progress) { - //TODO - } - - public void moveBlocks(Level level) { - forEachPreMove(p -> { - BlockMover mover = MovementRegistry.getMover(level, p); - mover.move(level, p, Direction.values()[dir]); - }); - } - - public void postMove(Level level) { - forEachPostMove(p -> { - BlockMover mover = MovementRegistry.getMover(level, p); - mover.postMove(level, p); - }); - } - - public void endMove(Level level) { - if (!level.isClientSide) { - forEachPostMove(p -> { - MovementController controller = MovementRegistry.getMovementController(level, pos); - if (controller != null) controller.onMovementFinished(level, pos); - }); - } - } - - public void addNeighborChanges(Level level, Set changes) { - forEachAll(p -> { - changes.add(p); - for (int s = 0; s < 6; s++) { - changes.add(p.relative(Direction.values()[s])); - } - }); - } - - private RowIterator iteratePreMove() { - return new RowIterator(1, size); - } - - private RowIterator iteratePostMove() { - return new RowIterator(0, size - 1); - } - - private RowIterator iterateAll() { - return new RowIterator(0, size); - } - - private void forEachPreMove(Consumer action) { - Iterator it = iteratePreMove(); - while (it.hasNext()) { - action.accept(it.next()); - } - } - - private void forEachPostMove(Consumer action) { - Iterator it = iteratePostMove(); - while (it.hasNext()) { - action.accept(it.next()); - } - } - - private void forEachAll(Consumer action) { - Iterator it = iterateAll(); - while (it.hasNext()) { - action.accept(it.next()); - } - } - - @Override - public String toString() { - return "MovingRow[" + - "pos={" + pos.getX() + ", " + pos.getY() + ", " + pos.getZ() + "}" + - ", size=" + size + ']'; - } - - private class RowIterator implements Iterator { - - private final int size; - private final BlockPos.MutableBlockPos mpos = new BlockPos.MutableBlockPos(); - private int i; - - public RowIterator(int start, int size) { - this.size = size; - this.i = start; - } - - @Override - public boolean hasNext() { - return i < size; - } - - @Override - public BlockPos next() { - return mpos.set(pos).move(Direction.values()[dir].getOpposite(), i++); - } - } - } - - public interface InternalMovementInfo extends MovementDescriptor { - - InternalMovementInfo NO_MOVEMENT_INFO = new InternalMovementInfo() { - //@formatter:off - @Override public Vector3 getRenderOffset(float partialTicks) { return Vector3.ZERO; } - @Override public MovementStatus getStatus() { return MovementStatus.UNKNOWN; } - @Override public boolean isMoving() { return false; } - @Override public double getProgress() { return 0; } - @Override public int getSize() { return 0; } - //@formatter:on - }; - - private static InternalMovementInfo failedMovement(int size) { - return new InternalMovementInfo() { - //@formatter:off - @Override public Vector3 getRenderOffset(float partialTicks) { return Vector3.ZERO; } - @Override public MovementStatus getStatus() { return MovementStatus.FAILED; } - @Override public boolean isMoving() { return false; } - @Override public double getProgress() { return 0; } - @Override public int getSize() { return size; } - //@formatter:on - }; - } - - Vector3 getRenderOffset(float partialTicks); - } } diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/MovingStructure.java b/expansion/src/main/java/mrtjp/projectred/expansion/MovingStructure.java new file mode 100644 index 000000000..f2d4137c5 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/MovingStructure.java @@ -0,0 +1,569 @@ +package mrtjp.projectred.expansion; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.vec.Vector3; +import mrtjp.projectred.api.BlockMover; +import mrtjp.projectred.api.MovementController; +import mrtjp.projectred.lib.VecLib; +import net.covers1624.quack.collection.FastStream; +import net.covers1624.quack.util.LazyValue; +import net.minecraft.client.Minecraft; +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.entity.MoverType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +import java.util.*; +import java.util.function.Consumer; + +import static mrtjp.projectred.api.MovementDescriptor.MovementStatus.*; + +/** + * A single group of moving blocks, created and managed by {@link MovementManager} + */ +public class MovingStructure implements MovingStructureInfo { + + public final int id; + + private final double speed; + private final int dir; + private final List rows; + private final int totalSize; + + private final LazyValue> intersectingChunks = new LazyValue<>(this::computeIntersectingChunks); + private final LazyValue> renderChunks = new LazyValue<>(this::computeRenderChunks); + + private MovementStatus status; + private double progress; + + private MovingStructure(int id, double speed, int dir, List rows, MovementStatus status, double progress) { + this.id = id; + this.speed = speed; + this.dir = dir; + this.rows = rows; + this.status = status; + this.progress = progress; + this.totalSize = FastStream.of(this.rows).intSum(r -> r.size); + } + + private MovingStructure(int id, double speed, int dir, List rows) { + this(id, speed, dir, rows, PENDING_START, 0D); + } + + public static MovingStructure fromBlockSet(int id, double speed, int dir, Set blocks) { + // Split set of blocks into rows going the opposite direction of the move + Set> rows = VecLib.resolveRows(blocks, dir^1); + + // Create MovingRows + List movingRows = new LinkedList<>(); + for (List row : rows) { + movingRows.add(new MovingRow(row, dir)); + } + + return new MovingStructure(id, speed, dir, Collections.unmodifiableList(movingRows), PENDING_START, 0); + } + + //region Network + public void writeDesc(MCDataOutput output) { + output.writeShort(id); + output.writeDouble(speed); + output.writeByte(dir); + output.writeShort(rows.size()); + for (MovingRow row : rows) { + output.writePos(row.pos); + output.writeShort(row.size); + } + output.writeByte(status.ordinal()); + output.writeDouble(progress); //TODO use integers instead + } + + public static MovingStructure fromDesc(MCDataInput input) { + int id = input.readUShort(); + double speed = input.readDouble(); + int dir = input.readUByte(); + int size = input.readUShort(); + List rows = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + rows.add(new MovingRow(input.readPos(), dir, input.readUShort())); + } + MovementStatus status = MovementStatus.values()[input.readUByte()]; + double progress = input.readDouble(); + + return new MovingStructure(id, speed, dir, rows, status, progress); + } + //endregion + + //region Movement description + @Override + public MovementStatus getStatus() { + return status; + } + + @Override + public boolean isMoving() { + return getStatus() == MOVING || getStatus() == PENDING_FINALIZATION; + } + + @Override + public double getProgress() { + return progress; + } + + @Override + public int getSize() { + return totalSize; + } + + @Override + public Direction getDirection() { + return Direction.values()[dir]; + } + + @Override + public Vector3 getMovementOffset() { + double p = Math.min(progress, 1D); + return Vector3.fromBlockPos(BlockPos.ZERO.relative(Direction.values()[dir])).multiply(p); + } + + @Override + public Vector3 getRenderOffset(float partialTicks) { + double p = Math.min(progress + speed * partialTicks, 1D); + return Vector3.fromBlockPos(BlockPos.ZERO.relative(Direction.values()[dir])).multiply(p); + } + //endregion + + //region Public accessors + public HashSet getChunkSet() { + return intersectingChunks.get(); + } + + public boolean intersects(ChunkPos pos) { + return intersectingChunks.get().contains(pos); + } + + public boolean contains(BlockPos pos) { + for (MovingRow row : rows) { + if (row.contains(pos)) return true; + } + return false; + } + + public Iterator iteratePreMove() { + return new Iterator<>() { + private final Iterator rowIterator = rows.iterator(); + private Iterator currentIterator = rowIterator.next().iteratePreMove(); + + @Override + public boolean hasNext() { + return currentIterator.hasNext() || rowIterator.hasNext(); + } + + @Override + public BlockPos next() { + if (!currentIterator.hasNext()) { + currentIterator = rowIterator.next().iteratePreMove(); + } + return currentIterator.next(); + } + }; + } + //endregion + + //region Structure management + public void tickProgress(Level level) { + + // Should not be ticking progress otherwise + assert status == MOVING || status == PENDING_FINALIZATION; + + if (status == MOVING) { + double oldProgress = progress; + progress = Math.min(progress + speed, 1D); + double delta = progress - oldProgress; + FastStream.of(rows).forEach(r -> r.pushEntities(level, progress, delta)); + + if (progress >= 1D) { + status = PENDING_FINALIZATION; + } + } + } + + public boolean canMove(Level level) { + for (MovingRow row : rows) { + if (!row.canMove(level)) return false; + } + return true; + } + + public void beginMove(Level level) { + assert status == MovementStatus.PENDING_START; + status = MOVING; + + FastStream.of(rows).forEach(r -> r.onMovementStarted(level)); + + if (level.isClientSide) { + // Force chunk to re-render so rendering of moving block can be suppressed by MovingBlockSuppressorRenderer + markChunksForRender(); + } + } + + public void executePreMove(Level level) { + // Silently moves blocks to new position + FastStream.of(rows).forEach(r -> r.moveBlocks(level)); + } + + public void executePostMove(Level level) { + // Completes the movement by alerting the tile itself, etc + FastStream.of(rows).forEach(r -> r.postMove(level)); + FastStream.of(rows).forEach(r -> r.onMovementFinished(level)); + + // Update neighbors + Set changes = new HashSet<>(); + FastStream.of(rows).forEach(r -> r.collectNeighborChanges(level, changes)); + + for (BlockPos pos : changes) { + BlockState state = level.getBlockState(pos); + state.updateNeighbourShapes(level, pos, 0, 0); + state.updateIndirectNeighbourShapes(level, pos, 0, 0); + level.neighborChanged(pos, Blocks.AIR, pos); //TODO use better context here + } + + // Update lighting + markBlocksForLightUpdate(level); + + // Update chunk rendering + if (level.isClientSide) { + markChunksForRender(); + } + + // Mark chunks as changed + for (ChunkPos p : getChunkSet()) { + level.getChunk(p.x, p.z).setUnsaved(true); + } + + //TODO Tick rescheduling + status = FINISHED; + } + + public void cancelMove(Level level) { + // Shouldn't need to do anything. Nothing happens until the animation is finished + // TODO MovementController notification for this? + assert status == MOVING || status == PENDING_FINALIZATION; + status = CANCELLED; + } + + @OnlyIn(Dist.CLIENT) + private void markChunksForRender() { + FastStream.of(renderChunks.get()).forEach(p -> Minecraft.getInstance().levelRenderer.setSectionDirty(p.x(), p.y(), p.z(), true)); + } + + private void markBlocksForLightUpdate(Level level) { + FastStream.of(rows).forEach(r -> r.forEachAll(p -> level.getLightEngine().checkBlock(p))); + } + //endregion + + //region Private utils + private HashSet computeIntersectingChunks() { + HashSet chunks = new HashSet<>(); + FastStream.of(rows).forEach(r -> r.forEachAll(p -> chunks.add(new ChunkPos(p)))); + return chunks; + } + + private HashSet computeRenderChunks() { + HashSet chunks = new HashSet<>(); + FastStream.of(rows).forEach(r -> r.forEachAll(p -> { + // Add all neighbors of blocks as well to update culled faces + for (int s = 0; s < 6; s++) { + chunks.add(SectionPos.of(p.relative(Direction.values()[s]))); + } + // Note: no need to add position itself, as it *must* be in one of above chunks + })); + return chunks; + } + //endregion + + @Override + public String toString() { + return "MovingStructure{" + + "id=" + id + + ", speed=" + speed + + ", dir=" + dir + + ", progress=" + progress + + ", rows=" + rows + + '}'; + } + + /** + * A single contiguous row of moving blocks + */ + private static final class MovingRow { + + /** + * Position of the head of the row. Empty before the move and becomes the first + * block that is moving post-move. + */ + public final BlockPos pos; + /** + * Direction that all blocks are moving towards. + */ + public final int dir; + /** + * Number of total blocks that are moving. + */ + public final int size; + + private MovingRow(BlockPos pos, int dir, int size) { + this.pos = pos; + this.dir = dir; + this.size = size; + } + + private MovingRow(List row, int dir) { + // Row's head should be the next block towards dir where everything will move, + // then followed by the rest of the row + this.pos = row.getFirst().relative(Direction.values()[dir]); + this.dir = dir; + this.size = row.size() + 1; + } + + //region Public accessors + public boolean contains(BlockPos pos) { + BlockPos p1 = VecLib.projectDir(this.pos, dir); + BlockPos p2 = VecLib.projectDir(pos, dir); + + // If projections towards dir plane are not equal, they cannot be on same axis + if (!p1.equals(p2)) return false; + + // pos is on the same line as this row. Check if its between start and end + int a1 = VecLib.rejectComponent(this.pos, dir); + int a2 = VecLib.rejectComponent(this.pos.relative(Direction.values()[dir ^ 1], size - 1), dir); + int b = VecLib.rejectComponent(pos, dir); + + return Math.min(a1, a2) <= b && b <= Math.max(a1, a2); + } + + public void collectNeighborChanges(Level level, Set changes) { + forEachAll(p -> { + changes.add(p); + for (int s = 0; s < 6; s++) { + changes.add(p.relative(Direction.values()[s])); + } + }); + } + //endregion + + //region Row management and control + + /** + * Check if this row can move. This involves checking for blockages, and asking each block's + * registered {@link MovementController} if the move is allowed. + * + * @param level The level + * @return True if nothing is blocking the movement and all controllers allow the movement + */ + public boolean canMove(Level level) { + if (!level.isLoaded(pos)) return false; + BlockState state = level.getBlockState(pos); + if (!(state.isAir() || state.canBeReplaced())) return false; + + Iterator it = iteratePreMove(); + while (it.hasNext()) { + BlockPos pos = it.next(); + + BlockMover mover = MovementRegistry.getMover(level, pos); + if (!mover.canMove(level, pos)) return false; + + MovementController controller = MovementRegistry.getMovementController(level, pos); + // Allow hooks to conditionally block movement + if (controller != null && !controller.isMovable(level, pos, Direction.values()[dir])) return false; + } + + return true; + } + + /** + * Called once movement has begun. This calls each block's {@link MovementController#onMovementStarted(Level, BlockPos, Direction)} + * + * @param level The level + */ + public void onMovementStarted(Level level) { + //TODO spawn movement blocks + + if (!level.isClientSide) { + // Notify blocks/BEs conforming to MovementController about move + forEachPreMove(p -> { + MovementController controller = MovementRegistry.getMovementController(level, p); + if (controller != null) controller.onMovementStarted(level, p, Direction.values()[dir]); + }); + } + } + + public void pushEntities(Level level, double progress, double delta) { + // Gather touching entities + var firstPos = pos.relative(Direction.values()[dir].getOpposite()); + var lastPos = pos.relative(Direction.values()[dir].getOpposite(), size); + var norm = Direction.values()[dir].getNormal(); + AABB aabb = AABB.encapsulatingFullBlocks(firstPos, lastPos) + .move(Vector3.fromVec3i(norm).multiply(progress).vec3()); + + // Box around entire row, plus a little more on top for standing entities + var searchBox = aabb.inflate( + norm.getX() * (1/16D), + 1/16D, + norm.getZ() * (1/16D)); + + level.getEntities((Entity) null, searchBox, e -> true).forEach(e -> { + // Calculate by how much the entity is colliding in dir of movement + var eBox = e.getBoundingBox(); + var collisionDist = switch (dir) { + case 0 -> eBox.maxY - aabb.minY; + case 1 -> aabb.maxY - eBox.minY; + case 2 -> eBox.maxZ - aabb.minZ; + case 3 -> aabb.maxZ - eBox.minZ; + case 4 -> eBox.maxX - aabb.minX; + case 5 -> aabb.maxX - eBox.minX; + default -> throw new IllegalStateException("Invalid movement direction: " + dir); + }; + + // If entity is intersecting, push it out of collision. Otherwise, push by progress % + boolean collides = aabb.intersects(eBox); + var pushDist = (collides ? Math.max(collisionDist, delta) : delta); + + // Push entity + Vector3 push = Vector3.fromVec3i(Direction.values()[dir].getNormal()).multiply(pushDist); + e.move(MoverType.PISTON, push.vec3()); + }); + } + + /** + * Calls each block's {@link BlockMover#move(Level, BlockPos, Direction)} to actually move the block. This + * phase typically silently re-locates the block or block entity without alerting neighbors. + * + * @param level The level + */ + public void moveBlocks(Level level) { + forEachPreMove(p -> { + BlockMover mover = MovementRegistry.getMover(level, p); + mover.move(level, p, Direction.values()[dir]); + }); + } + + /** + * Called after {@link #moveBlocks(Level)} has finished silently relocating all blocks. Calls + * {@link BlockMover#postMove(Level, BlockPos)} on each block's mover. + * + * @param level The level + */ + public void postMove(Level level) { + forEachPostMove(p -> { + BlockMover mover = MovementRegistry.getMover(level, p); + mover.postMove(level, p); + }); + } + + /** + * Calls each block's {@link MovementController#onMovementFinished(Level, BlockPos)} + * + * @param level The level + */ + public void onMovementFinished(Level level) { + if (!level.isClientSide) { + forEachPostMove(p -> { + MovementController controller = MovementRegistry.getMovementController(level, pos); + if (controller != null) controller.onMovementFinished(level, pos); + }); + } + } + //endregion + + //region Iterators + /** + * Iterates all blocks in their pre-move positions, starting at head position + */ + public Iterator iteratePreMove() { + return new MovingRow.RowIterator(1, size); + } + + /** + * Iterates all post-moved positions, starting at head position + */ + public Iterator iteratePostMove() { + return new MovingRow.RowIterator(0, size - 1); + } + + /** + * Iterates all positions, including initially empty head position. + */ + public Iterator iterateAll() { + return new MovingRow.RowIterator(0, size); + } + + public void forEachPreMove(Consumer action) { + var it = iteratePreMove(); + while (it.hasNext()) { + action.accept(it.next()); + } + } + + public void forEachPostMove(Consumer action) { + var it = iteratePostMove(); + while (it.hasNext()) { + action.accept(it.next()); + } + } + + public void forEachAll(Consumer action) { + var it = iterateAll(); + while (it.hasNext()) { + action.accept(it.next()); + } + } + //endregion + + @Override + public String toString() { + return "MovingRow{" + + "pos={" + pos.getX() + ", " + pos.getY() + ", " + pos.getZ() + "}" + + ", size=" + size + + ", dir=" + dir + + "}"; + } + + /** + * A ranged iterator for going through a portion of blocks in this row + */ + private class RowIterator implements Iterator { + + private final int size; + private final BlockPos.MutableBlockPos mpos = new BlockPos.MutableBlockPos(); + private int i; + + /** + * @param start Index of starting block + * @param size Total number of blocks to iterate + */ + public RowIterator(int start, int size) { + this.size = size; + this.i = start; + } + + @Override + public boolean hasNext() { + return i < size; + } + + @Override + public BlockPos next() { + return mpos.set(pos).move(Direction.values()[dir].getOpposite(), i++); + } + } + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/MovingStructureInfo.java b/expansion/src/main/java/mrtjp/projectred/expansion/MovingStructureInfo.java new file mode 100644 index 000000000..bb011af40 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/MovingStructureInfo.java @@ -0,0 +1,40 @@ +package mrtjp.projectred.expansion; + +import codechicken.lib.vec.Vector3; +import mrtjp.projectred.api.MovementDescriptor; +import net.minecraft.core.Direction; + +public interface MovingStructureInfo extends MovementDescriptor { + + MovingStructureInfo NO_MOVEMENT_INFO = new MovingStructureInfo() { + //@formatter:off + @Override public Vector3 getMovementOffset() { return Vector3.ZERO.copy(); } + @Override public Vector3 getRenderOffset(float partialTicks) { return Vector3.ZERO.copy(); } + @Override public MovementStatus getStatus() { return MovementStatus.UNKNOWN; } + @Override public boolean isMoving() { return false; } + @Override public double getProgress() { return 0; } + @Override public int getSize() { return 0; } + public Direction getDirection() { return Direction.DOWN; } + //@formatter:on + }; + + static MovingStructureInfo failedMovement(int size) { + return new MovingStructureInfo() { + //@formatter:off + @Override public Vector3 getMovementOffset() { return Vector3.ZERO.copy(); } + @Override public Vector3 getRenderOffset(float partialTicks) { return Vector3.ZERO.copy(); } + @Override public MovementStatus getStatus() { return MovementStatus.FAILED; } + @Override public boolean isMoving() { return false; } + @Override public double getProgress() { return 0; } + @Override public int getSize() { return size; } + @Override public Direction getDirection() { return Direction.DOWN; } + //@formatter:on + }; + } + + Direction getDirection(); + + Vector3 getMovementOffset(); + + Vector3 getRenderOffset(float partialTicks); +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameBlockModel.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameBlockModel.java new file mode 100644 index 000000000..645690ce4 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameBlockModel.java @@ -0,0 +1,94 @@ +package mrtjp.projectred.expansion.client; + +import codechicken.lib.model.PerspectiveModel; +import codechicken.lib.model.PerspectiveModelState; +import codechicken.lib.render.CCRenderState; +import codechicken.lib.render.buffer.BakedQuadVertexBuilder; +import codechicken.lib.util.TransformUtils; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.ItemOverrides; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.client.ChunkRenderTypeSet; +import net.neoforged.neoforge.client.model.IDynamicBakedModel; +import net.neoforged.neoforge.client.model.data.ModelData; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; + +public class FrameBlockModel implements IDynamicBakedModel, PerspectiveModel { + + // Mask -> List + private static final HashMap> bakedQuads = new HashMap<>(); + + public FrameBlockModel() { + } + + @Override + public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) { + // Cutout only + if (renderType != null && renderType != RenderType.cutout()) return List.of(); + + // No cull yet + if (side != null) return List.of(); + + var frameData = extraData.get(FrameModelData.DATA); + int mask = frameData != null ? frameData.mask() : 0; + + return getOrGenerateQuads(mask); + } + + @Override + public @Nullable PerspectiveModelState getModelState() { + return TransformUtils.DEFAULT_BLOCK; + } + + @Override + public ChunkRenderTypeSet getRenderTypes(BlockState state, RandomSource rand, ModelData data) { + return ChunkRenderTypeSet.of(RenderType.cutout()); + } + + private static List getOrGenerateQuads(int mask) { + var quads = bakedQuads.get(mask); + if (quads != null) return quads; + + synchronized (bakedQuads) { + // Re-check after waiting for sync + quads = bakedQuads.get(mask); + if (quads != null) return quads; + + quads = generateQuads(mask); + bakedQuads.put(mask, quads); + } + + return quads; + } + + private static List generateQuads(int mask) { + CCRenderState ccrs = CCRenderState.instance(); + ccrs.reset(); + ccrs.computeLighting = false; // No lighting whilst baking! + ccrs.brightness = 0; + BakedQuadVertexBuilder builder = new BakedQuadVertexBuilder(); + ccrs.bind(builder, DefaultVertexFormat.BLOCK); + FrameModelRenderer.renderStatic(ccrs, mask); + + return builder.bake(); + } + + //region BakedModel + //@formatter:off + @Override public boolean useAmbientOcclusion() { return true; } + @Override public boolean isGui3d() { return true; } + @Override public boolean usesBlockLight() { return true; } + @Override public boolean isCustomRenderer() { return false; } + @Override public TextureAtlasSprite getParticleIcon() { return FrameModelRenderer.getFrameIcon(); } + @Override public ItemOverrides getOverrides() { return ItemOverrides.EMPTY; } + //@formatter:on + //endregion +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameBlockRenderer.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameBlockRenderer.java deleted file mode 100644 index 81dff4e2b..000000000 --- a/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameBlockRenderer.java +++ /dev/null @@ -1,71 +0,0 @@ -package mrtjp.projectred.expansion.client; - -import codechicken.lib.model.PerspectiveModelState; -import codechicken.lib.render.CCRenderState; -import codechicken.lib.render.block.ICCBlockRenderer; -import codechicken.lib.render.buffer.TransformingVertexConsumer; -import codechicken.lib.render.item.IItemRenderer; -import codechicken.lib.util.TransformUtils; -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import mrtjp.projectred.expansion.init.ExpansionBlocks; -import net.minecraft.client.renderer.ItemBlockRenderTypes; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.core.BlockPos; -import net.minecraft.util.RandomSource; -import net.minecraft.world.item.ItemDisplayContext; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.BlockAndTintGetter; -import net.minecraft.world.level.block.state.BlockState; -import net.neoforged.neoforge.client.model.data.ModelData; -import org.jetbrains.annotations.Nullable; - -public class FrameBlockRenderer implements ICCBlockRenderer, IItemRenderer { - - public static final FrameBlockRenderer INSTANCE = new FrameBlockRenderer(); - - public FrameBlockRenderer() { - } - - //region ICCBlockRenderer - @Override - public boolean canHandleBlock(BlockAndTintGetter world, BlockPos pos, BlockState blockState, @Nullable RenderType renderType) { - return blockState.getBlock() == ExpansionBlocks.FRAME_BLOCK.get(); - } - - @Override - public void renderBlock(BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack mStack, VertexConsumer builder, RandomSource random, ModelData data, @Nullable RenderType renderType) { - CCRenderState ccrs = CCRenderState.instance(); - ccrs.reset(); - ccrs.bind(new TransformingVertexConsumer(builder, mStack), DefaultVertexFormat.BLOCK); - ccrs.lightMatrix.locate(world, pos); - ccrs.setBrightness(world, pos); - - FrameModelRenderer.renderStatic(ccrs, 0); - } - //endregion - - //region IItemRenderer - @Override - public void renderItem(ItemStack stack, ItemDisplayContext transformType, PoseStack mStack, MultiBufferSource source, int packedLight, int packedOverlay) { - CCRenderState ccrs = CCRenderState.instance(); - ccrs.reset(); - ccrs.brightness = packedLight; - ccrs.overlay = packedOverlay; - ccrs.bind(ItemBlockRenderTypes.getRenderType(stack, true), source, mStack); - - FrameModelRenderer.renderStatic(ccrs, 0); - } - - - - //@formatter:off - @Override public boolean useAmbientOcclusion() { return true; } - @Override public boolean isGui3d() { return true; } - @Override public boolean usesBlockLight() { return true; } - @Override public @Nullable PerspectiveModelState getModelState() { return TransformUtils.DEFAULT_BLOCK; } - //@formatter:on - //endregion -} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameModelData.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameModelData.java new file mode 100644 index 000000000..f7b7a10ea --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameModelData.java @@ -0,0 +1,7 @@ +package mrtjp.projectred.expansion.client; + +import net.neoforged.neoforge.client.model.data.ModelProperty; + +public record FrameModelData(int mask) { + public static final ModelProperty DATA = new ModelProperty<>(); +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameMotorBlockRenderer.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameMotorBlockModel.java similarity index 52% rename from expansion/src/main/java/mrtjp/projectred/expansion/client/FrameMotorBlockRenderer.java rename to expansion/src/main/java/mrtjp/projectred/expansion/client/FrameMotorBlockModel.java index a5db0facd..cbd381fe5 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameMotorBlockRenderer.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/client/FrameMotorBlockModel.java @@ -1,18 +1,13 @@ package mrtjp.projectred.expansion.client; -import codechicken.lib.model.PerspectiveModelState; -import codechicken.lib.util.TransformUtils; import codechicken.lib.vec.uv.MultiIconTransformation; import mrtjp.projectred.core.block.ProjectRedBlock; -import mrtjp.projectred.core.client.FullyOrientableBlockRenderer; +import mrtjp.projectred.core.client.FullyOrientableBlockModel; import mrtjp.projectred.expansion.init.ExpansionBlocks; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.block.state.BlockState; import net.neoforged.neoforge.client.event.TextureAtlasStitchedEvent; @@ -22,9 +17,7 @@ import static mrtjp.projectred.expansion.ProjectRedExpansion.MOD_ID; @SuppressWarnings("NotNullFieldNotInitialized") -public class FrameMotorBlockRenderer extends FullyOrientableBlockRenderer { - - public static final FrameMotorBlockRenderer INSTANCE = new FrameMotorBlockRenderer(); +public class FrameMotorBlockModel extends FullyOrientableBlockModel { private static TextureAtlasSprite topIcon; private static TextureAtlasSprite frontBack0Icon; @@ -38,23 +31,16 @@ public class FrameMotorBlockRenderer extends FullyOrientableBlockRenderer { private static @Nullable MultiIconTransformation iconT2; private static @Nullable MultiIconTransformation iconT3; - public FrameMotorBlockRenderer() { - } - @Override - public boolean canHandleBlock(BlockAndTintGetter world, BlockPos pos, BlockState blockState, @Nullable RenderType renderType) { - return blockState.getBlock() == ExpansionBlocks.FRAME_MOTOR_BLOCK.get(); - } - - @Override - protected RenderType getBlockRenderLayer(BlockState state, BlockPos pos, BlockAndTintGetter level) { + protected RenderType getBlockRenderLayer(@Nullable BlockState state) { return RenderType.solid(); } @Override - protected RenderData getBlockRenderData(BlockState state, BlockPos pos, BlockAndTintGetter level) { - - createIconTransforms(); + protected RenderData getBlockRenderData(@Nullable BlockState state) { + if (state == null) { + return new RenderData(0, 0, Objects.requireNonNull(iconT1)); + } int s = state.getValue(ProjectRedBlock.SIDE); int r = state.getValue(ProjectRedBlock.ROTATION); @@ -67,22 +53,16 @@ protected RenderData getBlockRenderData(BlockState state, BlockPos pos, BlockAnd } @Override - protected RenderData getItemRenderData(ItemStack stack) { - - createIconTransforms(); - return new RenderData(0, 0, Objects.requireNonNull(iconT1)); + protected BlockState getItemRenderState() { + return ExpansionBlocks.FRAME_MOTOR_BLOCK.get().defaultBlockState(); } - private void createIconTransforms() { - // If resources reloaded, re-create the icon transformations - if (iconT1 == null || iconT1.icons[0] != bottomIcon) { - iconT1 = new MultiIconTransformation(bottomIcon, topIcon, frontBack0Icon, frontBack0Icon, leftIcon, rightIcon); - iconT2 = new MultiIconTransformation(bottomIcon, topIcon, frontBack1Icon, frontBack1Icon, leftIcon, rightIcon); - iconT3 = new MultiIconTransformation(bottomIcon, topIcon, frontBack2Icon, frontBack2Icon, leftIcon, rightIcon); - } + @Override + public TextureAtlasSprite getParticleIcon() { + return topIcon; } - public static void onTextureStitchEvent(TextureAtlasStitchedEvent event) { + public static void onTextureStitchEvent(TextureAtlasStitchedEvent event) { if (!event.getAtlas().location().equals(TextureAtlas.LOCATION_BLOCKS)) return; topIcon = event.getAtlas().getSprite(ResourceLocation.fromNamespaceAndPath(MOD_ID, "block/frame_motor_top")); frontBack0Icon = event.getAtlas().getSprite(ResourceLocation.fromNamespaceAndPath(MOD_ID, "block/frame_motor_front_back_0")); @@ -91,23 +71,9 @@ public static void onTextureStitchEvent(TextureAtlasStitchedEvent event) { leftIcon = event.getAtlas().getSprite(ResourceLocation.fromNamespaceAndPath(MOD_ID, "block/frame_motor_left")); rightIcon = event.getAtlas().getSprite(ResourceLocation.fromNamespaceAndPath(MOD_ID, "block/frame_motor_right")); bottomIcon = event.getAtlas().getSprite(ResourceLocation.fromNamespaceAndPath(MOD_ID, "block/frame_motor_bottom")); - } - public static TextureAtlasSprite getParticleIcon(BlockState state, int side) { - return switch (side) { - case 0 -> bottomIcon; - case 1 -> topIcon; - case 2, 3 -> frontBack0Icon; - case 4 -> leftIcon; - case 5 -> rightIcon; - default -> throw new IllegalArgumentException("Invalid side: " + side); - }; + iconT1 = new MultiIconTransformation(bottomIcon, topIcon, frontBack0Icon, frontBack0Icon, leftIcon, rightIcon); + iconT2 = new MultiIconTransformation(bottomIcon, topIcon, frontBack1Icon, frontBack1Icon, leftIcon, rightIcon); + iconT3 = new MultiIconTransformation(bottomIcon, topIcon, frontBack2Icon, frontBack2Icon, leftIcon, rightIcon); } - - //@formatter:off - @Override public boolean useAmbientOcclusion() { return true; } - @Override public boolean isGui3d() { return true; } - @Override public boolean usesBlockLight() { return true; } - @Override public @Nullable PerspectiveModelState getModelState() { return TransformUtils.DEFAULT_BLOCK; } - //@formatter:on } diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/FramePartRenderer.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/FramePartRenderer.java deleted file mode 100644 index b40b231eb..000000000 --- a/expansion/src/main/java/mrtjp/projectred/expansion/client/FramePartRenderer.java +++ /dev/null @@ -1,23 +0,0 @@ -package mrtjp.projectred.expansion.client; - -import codechicken.lib.render.CCRenderState; -import codechicken.multipart.api.part.render.PartRenderer; -import mrtjp.projectred.expansion.part.FramePart; -import net.minecraft.client.renderer.RenderType; -import org.jetbrains.annotations.Nullable; - -public class FramePartRenderer implements PartRenderer { - - public static final PartRenderer INSTANCE = new FramePartRenderer(); - - private FramePartRenderer() { - } - - @Override - public void renderStatic(FramePart part, @Nullable RenderType type, CCRenderState ccrs) { - if (type == null || type == RenderType.cutout()) { - ccrs.setBrightness(part.level(), part.pos()); - FrameModelRenderer.renderStatic(ccrs, part.getOccludedSideMask()); - } - } -} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/MovingBlockRenderManager.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/MovingBlockRenderManager.java new file mode 100644 index 000000000..2d9749349 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/client/MovingBlockRenderManager.java @@ -0,0 +1,140 @@ +package mrtjp.projectred.expansion.client; + +import codechicken.lib.render.RenderUtils; +import codechicken.lib.render.buffer.TransformingVertexConsumer; +import codechicken.lib.vec.Vector3; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import mrtjp.projectred.expansion.MovementManager; +import mrtjp.projectred.expansion.MovingStructure; +import mrtjp.projectred.expansion.MovingStructureInfo; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.neoforge.client.event.RenderHighlightEvent; +import net.neoforged.neoforge.client.event.RenderLevelStageEvent; +import net.neoforged.neoforge.client.model.data.ModelData; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +public class MovingBlockRenderManager { + + public static boolean isMoving(MovementManager manager, BlockPos pos) { + return manager.getMovementInfo(pos).isMoving(); + } + + public static boolean isAdjacentToMoving(MovementManager manager, BlockPos pos) { + for (int s = 0; s < 6; s++) { + var rPos = pos.relative(Direction.values()[s]); + if (manager.getMovementInfo(rPos).isMoving()) return true; + } + return false; + } + + @OnlyIn(Dist.CLIENT) + public static void onRenderLevelStage(RenderLevelStageEvent event) { + Level level = Minecraft.getInstance().level; + if (level == null) return; + + MovementManager manager = MovementManager.getInstance(level); + if (manager.hasNoMovingStructures()) return; + + // Get the renderType for this stage, and skip if we dont care about it + List renderTypes = List.of(RenderType.solid(), RenderType.cutout(), RenderType.cutoutMipped(), RenderType.translucent()); + RenderType renderType = null; + for (RenderType type : renderTypes) { + if (RenderLevelStageEvent.Stage.fromRenderType(type) == event.getStage()) { + renderType = type; + break; + } + } + if (renderType == null) return; + + RandomSource random = RandomSource.create(); + + // Set up camera pose + Vec3 cam = event.getCamera().getPosition(); + PoseStack stack = event.getPoseStack(); + stack.pushPose(); + stack.mulPose(event.getModelViewMatrix()); + stack.translate(-cam.x, -cam.y, -cam.z); + + for (MovingStructure structure : manager.getMovingStructures()) { + + // Set up render offset based on progress of movement + Vector3 offset = structure.getRenderOffset(event.getPartialTick().getGameTimeDeltaPartialTick(false)); + stack.pushPose(); + stack.translate(offset.x, offset.y, offset.z); + + MultiBufferSource.BufferSource buffers = Minecraft.getInstance().renderBuffers().bufferSource(); + BlockRenderDispatcher blockRenderer = Minecraft.getInstance().getBlockRenderer(); + + Iterator it = structure.iteratePreMove(); + while (it.hasNext()) { + BlockPos p = it.next(); + BlockState state = level.getBlockState(p); + BakedModel model = blockRenderer.getBlockModel(state); + ModelData data = level.getModelData(p); + + if (!model.getRenderTypes(state, random, data).contains(renderType)) { + continue; + } + + // Render the moving block + stack.pushPose(); + stack.translate(p.getX(), p.getY(), p.getZ()); + blockRenderer.renderBatched(state, p, level, stack, buffers.getBuffer(renderType), false, random, data, renderType); + stack.popPose(); //p + } + + buffers.endBatch(); + stack.popPose(); //offset + } + + stack.popPose(); //cam + } + + @OnlyIn(Dist.CLIENT) + public static void onDrawHighlight(RenderHighlightEvent.Block event) { + // Check for movement. Return if not moving + MovementManager manager = MovementManager.getClientInstanceNullable(); + if (manager == null || manager.hasNoMovingStructures()) return; + MovingStructureInfo info = manager.getMovementInfo(event.getTarget().getBlockPos()); + if (!info.isMoving()) return; + + Level level = Objects.requireNonNull(Minecraft.getInstance().level); + BlockPos pos = event.getTarget().getBlockPos(); + BlockState state = level.getBlockState(pos); + if (state.isAir() || !level.getWorldBorder().isWithinBounds(pos)) return; + + Camera camera = event.getCamera(); + PoseStack pStack = event.getPoseStack(); + pStack.pushPose(); + pStack.translate(-camera.getPosition().x, -camera.getPosition().y, -camera.getPosition().z); + + VoxelShape shape = state.getShape(level, pos); + var offset = info.getRenderOffset(event.getDeltaTracker().getGameTimeDeltaPartialTick(false)); + pStack.translate(pos.getX(), pos.getY(), pos.getZ()); + pStack.translate(offset.x, offset.y, offset.z); + VertexConsumer consumer = new TransformingVertexConsumer(event.getMultiBufferSource().getBuffer(RenderType.lines()), pStack); + RenderUtils.bufferShapeOutline(consumer, shape, 0, 0, 0, 0.4F); // RGBA from LevelRenderer#renderHitOutline + + pStack.popPose(); + + event.setCanceled(true); + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/MovingBlockSuppressorRenderer.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/MovingBlockSuppressorRenderer.java deleted file mode 100644 index 24fb48ed8..000000000 --- a/expansion/src/main/java/mrtjp/projectred/expansion/client/MovingBlockSuppressorRenderer.java +++ /dev/null @@ -1,85 +0,0 @@ -package mrtjp.projectred.expansion.client; - -import codechicken.lib.render.block.ICCBlockRenderer; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import mrtjp.projectred.expansion.MovementManager; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.util.RandomSource; -import net.minecraft.world.level.BlockAndTintGetter; -import net.minecraft.world.level.block.state.BlockState; -import net.neoforged.neoforge.client.model.data.ModelData; -import org.jetbrains.annotations.Nullable; - -/** - * A global block renderer that takes care of temporarily suppressing rendering for any blocks that are - * currently moving, so the moving block renderer can take over. - */ -public class MovingBlockSuppressorRenderer implements ICCBlockRenderer { - - public static final MovingBlockSuppressorRenderer INSTANCE = new MovingBlockSuppressorRenderer(); - - public static boolean allowMovingRenderOnRenderThread = false; - - private static boolean isRendering = false; - - private MovingBlockSuppressorRenderer() { - } - - @Override - public boolean canHandleBlock(BlockAndTintGetter world, BlockPos pos, BlockState blockState, @Nullable RenderType renderType) { - // Infinite recursion prevention. #renderBlock is attempting a no-cull render. - if (isRendering) return false; - - MovementManager manager = MovementManager.getClientInstanceNullable(); - - // Do nothing if manager doesn't exist - if (manager == null || manager.hasNoMovingStructures()) return false; - - // Take over both moving and adjacent to moving blocks - boolean isMoving = isMoving(manager, pos); - - if (isMoving) { - // If we're on render thread and force flag is set, it means we're rendering the moving block with an offset. - // So return false here to prevent this from taking over and suppressing the render - if (RenderSystem.isOnRenderThread() && allowMovingRenderOnRenderThread) { - return false; - } - // Otherwise take over to block rendering - return true; - } - - // If adjacent to a moving block, take over to render without culling - return isAdjacentToMoving(manager, pos); - } - - @Override - public void renderBlock(BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack mStack, VertexConsumer builder, RandomSource random, ModelData data, @Nullable RenderType renderType) { - MovementManager manager = MovementManager.getClientInstanceNullable(); - - // Moving blocks don't render here - if (manager != null && isMoving(manager, pos)) return; - - // Render adjacent blocks without culling - isRendering = true; // Prevents ourselves from re-handling this event - // renderType should be nullable - //noinspection DataFlowIssue - Minecraft.getInstance().getBlockRenderer().renderBatched(state, pos, world, mStack, builder, false, random, data, renderType); - isRendering = false; - } - - private static boolean isMoving(MovementManager manager, BlockPos pos) { - return manager.getMovementInfo(pos).isMoving(); - } - - private static boolean isAdjacentToMoving(MovementManager manager, BlockPos pos) { - for (int s = 0; s < 6; s++) { - if (isMoving(manager, pos.relative(Direction.values()[s]))) return true; - } - return false; - } -} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/client/TubePartRenderer.java b/expansion/src/main/java/mrtjp/projectred/expansion/client/TubePartRenderer.java index f479c3f6a..e9c196bad 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/client/TubePartRenderer.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/client/TubePartRenderer.java @@ -33,7 +33,7 @@ public class TubePartRenderer implements PartRenderer { @Override public void renderStatic(BaseTubePart part, @Nullable RenderType layer, CCRenderState ccrs) { if (layer == null || (layer == RenderType.cutout() && part.useStaticRenderer())) { - ccrs.setBrightness(part.level(), part.pos()); + ccrs.brightness = 0; TubeModelRenderer.render(ccrs, part); } } diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/compatibility/EmbeddiumCompatibility.java b/expansion/src/main/java/mrtjp/projectred/expansion/compatibility/EmbeddiumCompatibility.java new file mode 100644 index 000000000..331fc330c --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/compatibility/EmbeddiumCompatibility.java @@ -0,0 +1,16 @@ +package mrtjp.projectred.expansion.compatibility; + +import org.embeddedt.embeddium.api.BlockRendererRegistry; + +import static mrtjp.projectred.expansion.ProjectRedExpansion.LOGGER; + +public class EmbeddiumCompatibility { + + public static void initClient(Object embeddiumModObject) { + LOGGER.info("Loading Project Red Embeddium Compatibility Module"); + + // Register renderer for handling Frame-moved blocks + BlockRendererRegistry.instance().registerRenderPopulator( + BlockRendererRegistry.RenderPopulator.forRenderer(new EmbeddiumMovingBlockRenderer())); + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/compatibility/EmbeddiumMovingBlockRenderer.java b/expansion/src/main/java/mrtjp/projectred/expansion/compatibility/EmbeddiumMovingBlockRenderer.java new file mode 100644 index 000000000..fcf7a4cbc --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/compatibility/EmbeddiumMovingBlockRenderer.java @@ -0,0 +1,40 @@ +package mrtjp.projectred.expansion.compatibility; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import mrtjp.projectred.expansion.MovementManager; +import mrtjp.projectred.expansion.client.MovingBlockRenderManager; +import net.minecraft.client.Minecraft; +import net.minecraft.util.RandomSource; +import org.embeddedt.embeddium.api.BlockRendererRegistry; +import org.embeddedt.embeddium.api.render.chunk.BlockRenderContext; + +/** + * Render hook for moving blocks for Embeddium render pipeline + */ +public class EmbeddiumMovingBlockRenderer implements BlockRendererRegistry.Renderer { + + @Override + public BlockRendererRegistry.RenderResult renderBlock(BlockRenderContext ctx, RandomSource randomSource, VertexConsumer vertexConsumer) { + + var blockRenderer = Minecraft.getInstance().getBlockRenderer(); + + // Fast path. If nothing is moving, bail + MovementManager manager = MovementManager.getClientInstanceNullable(); + if (manager == null || manager.hasNoMovingStructures()) { + return BlockRendererRegistry.RenderResult.PASS; + } + + // If block is moving, don't render it + if (MovingBlockRenderManager.isMoving(manager, ctx.pos())) { + return BlockRendererRegistry.RenderResult.OVERRIDE; + } + + // If block is adjacent to moving block, render without culling + if (MovingBlockRenderManager.isAdjacentToMoving(manager, ctx.pos())) { + blockRenderer.renderBatched(ctx.state(), ctx.pos(), ctx.localSlice(), ctx.stack(), vertexConsumer, false, randomSource, ctx.modelData(), ctx.renderLayer()); + return BlockRendererRegistry.RenderResult.OVERRIDE; + } + + return BlockRendererRegistry.RenderResult.PASS; + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionBlockStateModelProvider.java b/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionBlockStateModelProvider.java index e048a0e34..aad075bd5 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionBlockStateModelProvider.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionBlockStateModelProvider.java @@ -1,7 +1,11 @@ package mrtjp.projectred.expansion.data; +import codechicken.lib.datagen.ClassModelLoaderBuilder; import mrtjp.projectred.core.block.ProjectRedBlock; import mrtjp.projectred.expansion.block.BatteryBoxBlock; +import mrtjp.projectred.expansion.client.FrameBlockModel; +import mrtjp.projectred.expansion.client.FrameMotorBlockModel; +import net.minecraft.client.resources.model.BakedModel; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.data.PackOutput; import net.minecraft.world.level.block.Block; @@ -37,8 +41,8 @@ protected void registerStatesAndModels() { addSidedOppositeMatchingFacesDeviceBlock(DEPLOYER_BLOCK.get()); // Advanced models rendered programmatically. Only Particle info provided by model file - addProgrammaticWithParticleTexture(FRAME_BLOCK.get(), ""); - addProgrammaticWithParticleTexture(FRAME_MOTOR_BLOCK.get(), "_top"); + addDynamicModel(FRAME_BLOCK.get(), FrameBlockModel.class); + addDynamicModel(FRAME_MOTOR_BLOCK.get(), FrameMotorBlockModel.class); } private void addProgrammaticWithParticleTexture(Block block, String texSuffix) { @@ -50,6 +54,16 @@ private void addProgrammaticWithParticleTexture(Block block, String texSuffix) { simpleBlock(block, dummy); } + private void addDynamicModel(Block block, Class clazz) { + String blockName = BuiltInRegistries.BLOCK.getKey(block).getPath(); + ModelFile model = models() + .withExistingParent(blockName, "block") + .customLoader(ClassModelLoaderBuilder::new) + .clazz(clazz) + .end(); + simpleBlock(block, model); + } + private void addRotatableOppositeMatchingFacesBlock(Block block) { addRotatableVariants(block, createOppositeMatchingFaceModel(block)); } diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionItemModelProvider.java b/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionItemModelProvider.java index 6106c1a89..efa91ba76 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionItemModelProvider.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/data/ExpansionItemModelProvider.java @@ -2,8 +2,6 @@ import codechicken.lib.datagen.ItemModelProvider; import mrtjp.projectred.expansion.TubeType; -import mrtjp.projectred.expansion.client.FrameBlockRenderer; -import mrtjp.projectred.expansion.client.FrameMotorBlockRenderer; import mrtjp.projectred.expansion.client.TubePartItemRenderer; import net.minecraft.data.PackOutput; import net.minecraft.world.item.Item; @@ -37,8 +35,8 @@ protected void registerModels() { simpleItemBlock(BLOCK_BREAKER_BLOCK.get()); simpleItemBlock(DEPLOYER_BLOCK.get()); - clazz(FRAME_BLOCK.get(), FrameBlockRenderer.class); - clazz(FRAME_MOTOR_BLOCK.get(), FrameMotorBlockRenderer.class); + simpleItemBlock(FRAME_BLOCK.get()); + simpleItemBlock(FRAME_MOTOR_BLOCK.get()); for (TubeType type : TubeType.values()) { clazz(type.getItem(), TubePartItemRenderer.class); diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/init/ExpansionClientInit.java b/expansion/src/main/java/mrtjp/projectred/expansion/init/ExpansionClientInit.java index 58019ca88..d995979da 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/init/ExpansionClientInit.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/init/ExpansionClientInit.java @@ -1,31 +1,29 @@ package mrtjp.projectred.expansion.init; -import codechicken.lib.render.block.BlockRenderingRegistry; import codechicken.multipart.api.MultipartClientRegistry; +import codechicken.multipart.api.part.render.PartBakedModelRenderer; import mrtjp.projectred.expansion.GraphDebugManager; -import mrtjp.projectred.expansion.MovementManager; import mrtjp.projectred.expansion.TubeType; import mrtjp.projectred.expansion.client.*; +import mrtjp.projectred.expansion.compatibility.EmbeddiumCompatibility; import mrtjp.projectred.expansion.gui.screen.inventory.*; import mrtjp.projectred.expansion.item.BatteryBoxStorageComponent; import mrtjp.projectred.expansion.item.RecipePlanComponent; import net.covers1624.quack.util.SneakyUtils; -import net.minecraft.client.renderer.ItemBlockRenderTypes; -import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.resources.ResourceLocation; import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModList; import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.common.NeoForge; import static mrtjp.projectred.expansion.ProjectRedExpansion.MOD_ID; -import static mrtjp.projectred.expansion.init.ExpansionBlocks.*; +import static mrtjp.projectred.expansion.init.ExpansionBlocks.BATTERY_BOX_BLOCK; import static mrtjp.projectred.expansion.init.ExpansionItems.RECIPE_PLAN_ITEM; import static mrtjp.projectred.expansion.init.ExpansionMenus.*; import static mrtjp.projectred.expansion.init.ExpansionParts.FRAME_PART; -@SuppressWarnings("DataFlowIssue") public class ExpansionClientInit { public static final ResourceLocation ITEM_MODEL_PROPERTY_CHARGE_LEVEL = ResourceLocation.fromNamespaceAndPath(MOD_ID, "charge_level"); @@ -38,14 +36,15 @@ public static void init(IEventBus modEventBus) { modEventBus.addListener(ExpansionClientInit::onRegisterMenuScreensEvent); // MovementManager hooks - NeoForge.EVENT_BUS.addListener(MovementManager::onRenderLevelStage); + NeoForge.EVENT_BUS.addListener(MovingBlockRenderManager::onRenderLevelStage); + NeoForge.EVENT_BUS.addListener(MovingBlockRenderManager::onDrawHighlight); // GraphDebugManager hooks NeoForge.EVENT_BUS.addListener(GraphDebugManager::onRenderLevelStage); // Register sprites modEventBus.addListener(FrameModelRenderer::onTextureStitchEvent); - modEventBus.addListener(FrameMotorBlockRenderer::onTextureStitchEvent); + modEventBus.addListener(FrameMotorBlockModel::onTextureStitchEvent); modEventBus.addListener(PneumaticSmokeParticle::onTextureStitchEvent); for (var type : TubeType.values()) { modEventBus.addListener(type::onTextureStitchEvent); @@ -57,21 +56,19 @@ private static void clientSetup(final FMLClientSetupEvent event) { // Register item model properties addItemModelProperties(); - // Register block renderers - ItemBlockRenderTypes.setRenderLayer(FRAME_BLOCK.get(), RenderType.cutout()); - BlockRenderingRegistry.registerRenderer(FRAME_BLOCK.get(), FrameBlockRenderer.INSTANCE); - ItemBlockRenderTypes.setRenderLayer(FRAME_MOTOR_BLOCK.get(), RenderType.solid()); - BlockRenderingRegistry.registerRenderer(FRAME_MOTOR_BLOCK.get(), FrameMotorBlockRenderer.INSTANCE); - BlockRenderingRegistry.registerGlobalRenderer(MovingBlockSuppressorRenderer.INSTANCE); - // Register part renderers - MultipartClientRegistry.register(FRAME_PART.get(), FramePartRenderer.INSTANCE); + MultipartClientRegistry.register(FRAME_PART.get(), PartBakedModelRenderer.simple()); // Register pipe renderers for (TubeType type : TubeType.values()) { // Block renderer MultipartClientRegistry.register(type.getPartType(), SneakyUtils.unsafeCast(TubePartRenderer.INSTANCE)); } + + // Load compatibility modules + //noinspection Convert2MethodRef + ModList.get().getModContainerById("embeddium") + .ifPresent(mod -> EmbeddiumCompatibility.initClient(mod)); } private static void onRegisterMenuScreensEvent(RegisterMenuScreensEvent event) { diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockCollisionsMixin.java b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockCollisionsMixin.java new file mode 100644 index 000000000..fa7350cd1 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockCollisionsMixin.java @@ -0,0 +1,63 @@ +package mrtjp.projectred.expansion.mixin; + +import codechicken.lib.vec.Vector3; +import mrtjp.projectred.expansion.MovementManager; +import mrtjp.projectred.expansion.MovingStructureInfo; +import mrtjp.projectred.expansion.ProjectRedExpansion; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockCollisions; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(BlockCollisions.class) +public abstract class BlockCollisionsMixin { + + @Redirect( + method = "computeNext", + at = @At( + value = "INVOKE", + target = "net/minecraft/world/level/block/state/BlockState.getCollisionShape (Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/phys/shapes/CollisionContext;)Lnet/minecraft/world/phys/shapes/VoxelShape;" + ) + ) + private VoxelShape modifyCollisionShapeForMovingBlocks( + BlockState state, + BlockGetter getter, + BlockPos pos, + CollisionContext context) { + + VoxelShape shape = state.getCollisionShape(getter, pos, context); + + if (!(getter instanceof Level level)) return shape; + + MovementManager manager = MovementManager.getInstanceNullable(level); + if (manager == null) return shape; + + MovingStructureInfo info = manager.getMovementInfo(pos); + if (!info.isMoving()) return shape; + + // Merge in adjacent block's shape if moving in same direction + var adjacentOffset = info.getDirection().getOpposite(); + BlockPos pos2 = pos.relative(info.getDirection().getOpposite()); + MovingStructureInfo info2 = manager.getMovementInfo(pos2); + if (info2.isMoving() && info2.getDirection() == info.getDirection()) { + var shape2 = level.getBlockState(pos2).getCollisionShape(level, pos2, context); + shape2 = shape2.move(adjacentOffset.getStepX(), adjacentOffset.getStepY(), adjacentOffset.getStepZ()); + var shape3 = Shapes.or(shape, shape2); + ProjectRedExpansion.LOGGER.info("{}@{} adjacent block merged: {} + {} = {}", state, pos, shape, shape2, shape3); + shape = shape3; + } else { + ProjectRedExpansion.LOGGER.info("{}@{} adjacent block not moving: {}", state, pos, shape); + } + + // Apply offset to shape + Vector3 offset = info.getMovementOffset(); + return shape.move(offset.x, offset.y, offset.z); + } +} \ No newline at end of file diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockEntityRenderDispatcherMixin.java b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockEntityRenderDispatcherMixin.java index 63319bad9..20b8ebdb0 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockEntityRenderDispatcherMixin.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockEntityRenderDispatcherMixin.java @@ -2,6 +2,7 @@ import codechicken.lib.vec.Vector3; import com.mojang.blaze3d.vertex.PoseStack; +import mrtjp.projectred.expansion.MovingStructureInfo; import mrtjp.projectred.expansion.MovementManager; import mrtjp.projectred.expansion.client.MovementClientRegistry; import net.minecraft.client.renderer.MultiBufferSource; @@ -31,7 +32,7 @@ private static void wrapSetupAndRender(BlockEntityRender } // Get movement info - MovementManager.InternalMovementInfo info = MovementManager.getInstance(blockEntity.getLevel()).getMovementInfo(blockEntity.getBlockPos()); + MovingStructureInfo info = MovementManager.getInstance(blockEntity.getLevel()).getMovementInfo(blockEntity.getBlockPos()); // Translate stack if moving if (info.isMoving()) { @@ -50,19 +51,4 @@ private static void wrapSetupAndRender(BlockEntityRender MovementClientRegistry.dispatchPostRender(); } } - -// @Inject( -// method = "setupAndRender(Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderer;Lnet/minecraft/world/level/block/entity/BlockEntity;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;)V", -// at = @At("HEAD"), -// cancellable = true -// ) -// private static void preSetupAndRender(BlockEntityRenderer renderer, E blockEntity, float partialTicks, PoseStack pStack, MultiBufferSource buffers, CallbackInfo ci) { -// } -// -// @Inject( -// method = "setupAndRender(Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderer;Lnet/minecraft/world/level/block/entity/BlockEntity;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;)V", -// at = @At("RETURN") -// ) -// private static void postSetupAndRender(BlockEntityRenderer renderer, E blockEntity, float partialTicks, PoseStack pStack, MultiBufferSource buffers, CallbackInfo ci) { -// } } diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockGetterMixin.java b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockGetterMixin.java new file mode 100644 index 000000000..57603fa81 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/BlockGetterMixin.java @@ -0,0 +1,139 @@ +package mrtjp.projectred.expansion.mixin; + +import codechicken.lib.vec.Vector3; +import mrtjp.projectred.expansion.MovementManager; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import javax.annotation.Nullable; +import java.util.LinkedList; +import java.util.List; + +import static net.minecraft.world.level.BlockGetter.traverseBlocks; + +@Mixin(BlockGetter.class) +public interface BlockGetterMixin { + + @Inject(method = "clip", at = @At(value = "HEAD"), cancellable = true) + private void onClip(ClipContext clipContext, CallbackInfoReturnable cir) { + // Deal with movement only in active level + if (!(this instanceof Level level)) return; + + MovementManager manager = MovementManager.getInstanceNullable(level); + if (manager == null || manager.hasNoMovingStructures()) return; + + var hit = traverseBlocks(clipContext.getFrom(), clipContext.getTo(), clipContext, (ctx, pos) -> { + + var movementInfo = manager.getMovementInfo(pos); + if (!movementInfo.isMoving()) { + return lambda$clip$2(ctx, pos); // Super's impl of this lambda + } + + Vec3 fromVec = ctx.getFrom(); + Vec3 toVec = ctx.getTo(); + + List hits = new LinkedList<>(); + + // Hit test against block leaving the space + Vector3 offset = movementInfo.getMovementOffset(); + gatherHits(level, ctx, fromVec, toVec, pos, pos, offset, hits); + + // Hit test against the incomming block + Direction incomingDir = movementInfo.getDirection().getOpposite(); + BlockPos incomingPos = pos.relative(incomingDir); + Vector3 incomingOffset = movementInfo.getMovementOffset().add(incomingDir.getNormal()); + gatherHits(level, ctx, fromVec, toVec, pos, incomingPos, incomingOffset, hits); + + return selectClosest(hits, fromVec); + + }, ctx -> { + Vec3 vec3 = ctx.getFrom().subtract(ctx.getTo()); + return BlockHitResult.miss(ctx.getTo(), Direction.getNearest(vec3.x, vec3.y, vec3.z), BlockPos.containing(ctx.getTo())); + }); + + cir.setReturnValue(hit); + } + + /** + * Standard Block and fluid hit testing with option to provide a different block pos to provide the shapes from the + * position the clip testing is done, and an optional shape offset. + *

+ * Standard clip test behavior occurs when clipPos == blockPos and shapeOffset == (0,0,0). + * + * @param level The level from which shapes will be queried via blockPos + * @param ctx The clip context + * @param fromVec Clip look vector start + * @param toVec Clip look vector end + * @param clipPos Block position of the clip in relation to fromVec and toVec + * @param blockPos Position of block providing the shapes (returned as position in BlockHitResult) + * @param shapeOffset Offset to apply to the shape before hit testing + * @param hits List that receives the hits + */ + @Unique + private void gatherHits(Level level, ClipContext ctx, Vec3 fromVec, Vec3 toVec, BlockPos clipPos, BlockPos blockPos, Vector3 shapeOffset, List hits) { + BlockState blockstate = level.getBlockState(blockPos); + VoxelShape blockShape = ctx.getBlockShape(blockstate, level, blockPos); + VoxelShape interactShape = blockstate.getInteractionShape(level, blockPos); + BlockHitResult blockHit = clipWithOffsetAndOverride(fromVec, toVec, clipPos, shapeOffset, blockShape, interactShape); + if (blockHit != null) hits.add(blockHit.withPosition(blockPos)); + + FluidState fluidstate = level.getFluidState(blockPos); + VoxelShape fluidShape = ctx.getFluidShape(fluidstate, level, blockPos); + BlockHitResult fluidHit = clipWithOffsetAndOverride(fromVec, toVec, clipPos, shapeOffset, fluidShape, null); + if (fluidHit != null) hits.add(fluidHit.withPosition(blockPos)); + } + + @Unique + @Nullable + private BlockHitResult clipWithOffsetAndOverride(Vec3 from, Vec3 to, BlockPos pos, Vector3 offset, VoxelShape primaryShape, @Nullable VoxelShape override) { + var offsetPrimary = primaryShape.move(offset.x, offset.y, offset.z); + BlockHitResult hit = offsetPrimary.clip(from, to, pos); + if (hit != null && override != null) { + var offsetOverride = override.move(offset.x, offset.y, offset.z); + BlockHitResult overrideHit = offsetOverride.clip(from, to, pos); + if (overrideHit != null && overrideHit.getLocation().subtract(from).lengthSqr() < hit.getLocation().subtract(from).lengthSqr()) { + // Primary hit with direction from override hit + return hit.withDirection(overrideHit.getDirection()); + } + } + return hit; + } + + @Nullable + @Unique + private BlockHitResult selectClosest(List hits, Vec3 from) { + if (hits.isEmpty()) return null; + if (hits.size() == 1) return hits.getFirst(); + + double closestDist = Double.MAX_VALUE; + BlockHitResult closestHit = null; + for (BlockHitResult hit : hits) { + double dist = from.distanceToSqr(hit.getLocation()); + if (dist < closestDist) { + closestDist = dist; + closestHit = hit; + } + } + return closestHit; + } + + @Shadow + private BlockHitResult lambda$clip$2(ClipContext clipContext, BlockPos pos) { + //noinspection DataFlowIssue + return null; + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SectionCompilerMixin.java b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SectionCompilerMixin.java new file mode 100644 index 000000000..b250b7514 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SectionCompilerMixin.java @@ -0,0 +1,62 @@ +package mrtjp.projectred.expansion.mixin; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import mrtjp.projectred.expansion.MovementManager; +import mrtjp.projectred.expansion.client.MovingBlockRenderManager; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.chunk.SectionCompiler; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.client.model.data.ModelData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Render hook for moving blocks for Minecraft's default render pipeline + */ +@Mixin(SectionCompiler.class) +public class SectionCompilerMixin { + + @Redirect( + method = "compile(Lnet/minecraft/core/SectionPos;Lnet/minecraft/client/renderer/chunk/RenderChunkRegion;Lcom/mojang/blaze3d/vertex/VertexSorting;Lnet/minecraft/client/renderer/SectionBufferBuilderPack;Ljava/util/List;)Lnet/minecraft/client/renderer/chunk/SectionCompiler$Results;", + at = @At( + value = "INVOKE", + target = "net/minecraft/client/renderer/block/BlockRenderDispatcher.renderBatched (Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/BlockAndTintGetter;Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;ZLnet/minecraft/util/RandomSource;Lnet/neoforged/neoforge/client/model/data/ModelData;Lnet/minecraft/client/renderer/RenderType;)V" + ) + ) + private static void compile_WrapRenderBatched( + BlockRenderDispatcher blockRenderer, + BlockState state, + BlockPos pos, + BlockAndTintGetter blockAndTintGetter, + PoseStack poseStack, + VertexConsumer buffer, + boolean enableCulling, + RandomSource random, + ModelData modelData, + RenderType renderType) { + + MovementManager manager = MovementManager.getClientInstanceNullable(); + + if (manager != null && !manager.hasNoMovingStructures()) { + // If block is moving, don't render it + if (MovingBlockRenderManager.isMoving(manager, pos)) { + return; + } + + // If block is adjacent to moving block, render without culling + if (MovingBlockRenderManager.isAdjacentToMoving(manager, pos)) { + blockRenderer.renderBatched(state, pos, blockAndTintGetter, poseStack, buffer, false, random, modelData, renderType); + return; + } + } + + // Render normally + blockRenderer.renderBatched(state, pos, blockAndTintGetter, poseStack, buffer, enableCulling, random, modelData, renderType); + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SodiumAbstractBlockRenderContextMixin.java b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SodiumAbstractBlockRenderContextMixin.java new file mode 100644 index 000000000..16259d794 --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SodiumAbstractBlockRenderContextMixin.java @@ -0,0 +1,27 @@ +package mrtjp.projectred.expansion.mixin; + +import net.minecraft.core.Direction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.EnumSet; + +/** + * Provides a mechanism to supress culling on certain sides. Used by {@link SodiumBlockRendererMixin}. + */ +@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.frapi.render.AbstractBlockRenderContext") +public abstract class SodiumAbstractBlockRenderContextMixin { + + @Unique + protected EnumSet cullDisabledSides = EnumSet.noneOf(Direction.class); + + @Inject(method = "isFaceCulled", at = @At("HEAD"), cancellable = true) + public void isFaceCulled(Direction face, CallbackInfoReturnable cir) { + if (cullDisabledSides.contains(face)) { + cir.setReturnValue(false); + } + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SodiumBlockRendererMixin.java b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SodiumBlockRendererMixin.java new file mode 100644 index 000000000..4c35e416a --- /dev/null +++ b/expansion/src/main/java/mrtjp/projectred/expansion/mixin/SodiumBlockRendererMixin.java @@ -0,0 +1,57 @@ +package mrtjp.projectred.expansion.mixin; + +import mrtjp.projectred.expansion.MovementManager; +import mrtjp.projectred.expansion.client.MovingBlockRenderManager; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; +import net.caffeinemc.mods.sodium.client.world.LevelSlice; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.BlockState; +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; + +/** + * Hooks into Sodium's render pipeline to conditionally disable rendering moving blocks. Works with + * {@link SodiumAbstractBlockRenderContextMixin} to disable culling on moving sides. + */ +@Mixin(BlockRenderer.class) +public abstract class SodiumBlockRendererMixin extends SodiumAbstractBlockRenderContextMixin { + + @Inject(method = "prepare", at = @At("HEAD")) + private void prepare(ChunkBuildBuffers buffers, LevelSlice level, TranslucentGeometryCollector collector, CallbackInfo ci) { + cullDisabledSides.clear(); + } + + @Inject(method = "release", at = @At("HEAD")) + private void release(CallbackInfo ci) { + cullDisabledSides.clear(); + } + + @Inject(method = "renderModel", at = @At("HEAD"), cancellable = true) + private void preRenderModel(BakedModel model, BlockState state, BlockPos pos, BlockPos origin, CallbackInfo ci) { + // Fast path. If nothing is moving, bail + MovementManager manager = MovementManager.getClientInstanceNullable(); + if (manager == null || manager.hasNoMovingStructures()) { + return; // Proceeds to normal path + } + + // If block is moving, don't render it + if (MovingBlockRenderManager.isMoving(manager, pos)) { + ci.cancel(); + } + + // If any neighbor is moving, disable culling on that side + for (int s = 0; s < 6; s++) { + Direction side = Direction.values()[s]; + var sidePos = pos.relative(side); + if (MovingBlockRenderManager.isMoving(manager, sidePos)) { + cullDisabledSides.add(side); + } + } + } +} diff --git a/expansion/src/main/java/mrtjp/projectred/expansion/part/FramePart.java b/expansion/src/main/java/mrtjp/projectred/expansion/part/FramePart.java index f1e46ea7b..3231decc1 100644 --- a/expansion/src/main/java/mrtjp/projectred/expansion/part/FramePart.java +++ b/expansion/src/main/java/mrtjp/projectred/expansion/part/FramePart.java @@ -13,6 +13,7 @@ import codechicken.multipart.util.PartRayTraceResult; import mrtjp.projectred.api.Frame; import mrtjp.projectred.expansion.block.FrameBlock; +import mrtjp.projectred.expansion.client.FrameModelData; import mrtjp.projectred.expansion.client.FrameModelRenderer; import mrtjp.projectred.expansion.init.ExpansionParts; import net.minecraft.client.renderer.texture.TextureAtlasSprite; @@ -25,17 +26,19 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; +import net.neoforged.neoforge.client.model.data.ModelData; import org.jetbrains.annotations.Nullable; import java.util.Collections; import static mrtjp.projectred.expansion.init.ExpansionBlocks.FRAME_BLOCK; -public class FramePart extends BaseMultipart implements NormalOcclusionPart, IconHitEffectsPart, Frame { +public class FramePart extends BaseMultipart implements NormalOcclusionPart, IconHitEffectsPart, Frame, ModelRenderPart { public static final Cuboid6[] oBounds = new Cuboid6[6]; public static final VoxelShape[] oShapes = new VoxelShape[6]; @@ -228,4 +231,20 @@ public TextureAtlasSprite getBrokenIcon(int side) { return FrameModelRenderer.getFrameIcon(); } //endregion + + //region Model properties + @Override + public BlockState getCurrentState() { + // Bind to Frame block's model + return FRAME_BLOCK.get().defaultBlockState(); + } + + @Override + public ModelData getModelData() { + return ModelData.of( + FrameModelData.DATA, + new FrameModelData(getOccludedSideMask()) + ); + } + //endregion } diff --git a/expansion/src/main/resources/META-INF/neoforge.mods.toml b/expansion/src/main/resources/META-INF/neoforge.mods.toml index 7549eea51..96953c62c 100644 --- a/expansion/src/main/resources/META-INF/neoforge.mods.toml +++ b/expansion/src/main/resources/META-INF/neoforge.mods.toml @@ -43,4 +43,10 @@ Redstone. The way it was meant to be. type="required" versionRange="[${file.jarVersion},)" ordering="AFTER" - side="BOTH" \ No newline at end of file + side="BOTH" +[[dependencies.projectred_expansion]] + modId="embeddium" + type="optional" + versionRange="[${embeddium_version},)" + ordering="AFTER" + side="CLIENT" diff --git a/expansion/src/main/resources/mixins.projectred.expansion.json b/expansion/src/main/resources/mixins.projectred.expansion.json index eb89522be..1be116d7e 100644 --- a/expansion/src/main/resources/mixins.projectred.expansion.json +++ b/expansion/src/main/resources/mixins.projectred.expansion.json @@ -1,10 +1,9 @@ { - "required": true, - "minVersion": "0.8", - "package": "mrtjp.projectred.expansion.mixin", - "target": "@env(DEFAULT)", - "compatibilityLevel": "JAVA_17", - "client": [ - "BlockEntityRenderDispatcherMixin" - ] + "required": true, + "minVersion": "0.8", + "package": "mrtjp.projectred.expansion.mixin", + "target": "@env(DEFAULT)", + "compatibilityLevel": "JAVA_17", + "client": [ "BlockEntityRenderDispatcherMixin", "SectionCompilerMixin", "SodiumAbstractBlockRenderContextMixin", "SodiumBlockRendererMixin" ], + "mixins": [ "BlockCollisionsMixin", "BlockGetterMixin" ] } diff --git a/gradle.properties b/gradle.properties index d172bc9fd..6f2bb9b40 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,8 @@ cbm_version=3.5.0.+ jei_version=19.21.1.248 cct_version=1.116.1 +sodium_version=mc1.21.1-0.6.13-neoforge +embeddium_version=1.0.15+mc1.21.1 fabrication_version=0.1.0-alpha-19 diff --git a/illumination/src/main/java/mrtjp/projectred/illumination/client/MultipartLightPartRenderer.java b/illumination/src/main/java/mrtjp/projectred/illumination/client/MultipartLightPartRenderer.java index 4d530e4f4..2badec7d3 100644 --- a/illumination/src/main/java/mrtjp/projectred/illumination/client/MultipartLightPartRenderer.java +++ b/illumination/src/main/java/mrtjp/projectred/illumination/client/MultipartLightPartRenderer.java @@ -20,7 +20,7 @@ private MultipartLightPartRenderer() { @Override public void renderStatic(MultipartLightPart part, @Nullable RenderType layer, CCRenderState ccrs) { if (layer == null || layer == RenderType.cutout()) { - ccrs.setBrightness(part.level(), part.pos()); + ccrs.brightness = 0; part.getProperties().render(part, Vector3.ZERO, ccrs); } } diff --git a/integration/src/main/java/mrtjp/projectred/integration/client/GatePartRenderer.java b/integration/src/main/java/mrtjp/projectred/integration/client/GatePartRenderer.java index 33dd6f5aa..78281fdf5 100644 --- a/integration/src/main/java/mrtjp/projectred/integration/client/GatePartRenderer.java +++ b/integration/src/main/java/mrtjp/projectred/integration/client/GatePartRenderer.java @@ -23,7 +23,7 @@ private GatePartRenderer() { @Override public void renderStatic(GatePart part, @Nullable RenderType layer, CCRenderState ccrs) { if (layer == null || (layer == RenderType.cutout() && Configurator.CLIENT.staticGates.get())) { - ccrs.setBrightness(part.level(), part.pos()); + ccrs.brightness = 0; GateModelRenderer.instance().renderStatic(ccrs, part, RedundantTransformation.INSTANCE); } } diff --git a/transmission/src/main/java/mrtjp/projectred/transmission/client/CenterWirePartRenderer.java b/transmission/src/main/java/mrtjp/projectred/transmission/client/CenterWirePartRenderer.java index 9573be2e9..e17da53f0 100644 --- a/transmission/src/main/java/mrtjp/projectred/transmission/client/CenterWirePartRenderer.java +++ b/transmission/src/main/java/mrtjp/projectred/transmission/client/CenterWirePartRenderer.java @@ -18,7 +18,7 @@ public class CenterWirePartRenderer implements PartRenderer @Override public void renderStatic(BaseCenterWirePart part, @Nullable RenderType layer, CCRenderState ccrs) { if (layer == null || (layer == RenderType.cutout() && part.useStaticRenderer())) { - ccrs.setBrightness(part.level(), part.pos()); + ccrs.brightness = 0; FramedWireModelRenderer.render(ccrs, part); } } diff --git a/transmission/src/main/java/mrtjp/projectred/transmission/client/FaceWirePartRenderer.java b/transmission/src/main/java/mrtjp/projectred/transmission/client/FaceWirePartRenderer.java index e02ca9d39..a237dcc79 100644 --- a/transmission/src/main/java/mrtjp/projectred/transmission/client/FaceWirePartRenderer.java +++ b/transmission/src/main/java/mrtjp/projectred/transmission/client/FaceWirePartRenderer.java @@ -20,7 +20,7 @@ public class FaceWirePartRenderer implements PartRenderer { @Override public void renderStatic(BaseFaceWirePart part, @Nullable RenderType layer, CCRenderState ccrs) { if (layer == null || (layer == RenderType.solid() && useStaticRenderer(part))) { - ccrs.setBrightness(part.level(), part.pos()); + ccrs.brightness = 0; WireModelRenderer.render(ccrs, part); } }