diff --git a/README.md b/README.md index 6231d99..2d9c3b9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # Bedframe -## Premise +> [!WARNING] +> Bedframe is in early development, and you should *expect* problems. Batteries not included. See store for details. Translation layer that converts textured/custom server-side Polymer blocks and items to native Bedrock representations. +> [!NOTE] +> If you are looking for general modding support on bedrock, see if [Hydraulic](https://github.com/GeyserMC/Hydraulic) works for you. Their conversion code is better than mine. Do note that Hydraulic *does not* support Polymer mods at the moment, and I haven't tested if both mods work at the same time. + ## Requirements - [Geyser-Fabric](https://geysermc.org/download) @@ -13,22 +17,17 @@ You also probably want Polymer's Auto-Host feature, but Bedframe doesn't require ## Compatibility -- Works perfectly with [More Furnaces (Polymer)](https://modrinth.com/mod/morefurnaces)* -- Works perfectly with [Televator](https://modrinth.com/mod/televator) -- Will probably work with most simple Polymer Resource Pack mods - -*technically there is an item display for upgrades, but its hardly noticeable +Polymer mods that use display entities are not supported at the moment. (Geyser simply doesn't support it). So far I've tested the following: -## Limitations & Goals +- [Tom's Server Additions](https://modrinth.com/mods?q=Tom%27s+Server+Additions) (EXCLUDING Decorations and Furniture) +- [More Furnaces](https://modrinth.com/mod/morefurnaces) +- [Televator](https://modrinth.com/mod/televator) +- [Navigation Compasses](https://modrinth.com/mod/navigation-compasses) (might be under review) -- Custom models (a.k.a non-full-block or cross-block) do not work -- Entities are not touched -- Mods that use block displays don't work +Bedframe does not touch non-textured blocks (so mods like [Server-Side Waystones](https://modrinth.com/mod/sswaystones) will work as expected) -### Mods -- [Borukva-Food](https://github.com/MykhailoOpryshok/Borukva-Food/tree/master) is missing many models/textures (mostly crops and 3D tools) -- [Tom's Server Additions](https://modrinth.com/mods?q=Tom%27s+Server+Additions) is quite *iffy* +Feel free to [make an issue](https://github.com/sylvxa/bedframe/issues/new) for incompatible mods. ## License -This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own project \ No newline at end of file +This mod is available under the CC0 license. Feel free to learn from it and incorporate it in your own project \ No newline at end of file diff --git a/build.gradle b/build.gradle index 84fe47d..dee1fa2 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,11 @@ repositories { maven { url = uri("https://repo.opencollab.dev/main/") } + + // Serves "creative" library, needed for pack converter + maven { + url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + } } sourceSets { @@ -75,6 +80,11 @@ dependencies { // Geyser compileOnly("org.geysermc.geyser:api:${project.geyser_version}") compileOnly("org.geysermc.geyser:core:${project.geyser_version}") + + include api("org.geysermc.pack:converter:${project.pack_converter_version}") + include api("org.geysermc.pack:pack-schema-api:${project.pack_converter_version}") + include api("team.unnamed:creative-api:1.8.2-SNAPSHOT") + include api("team.unnamed:creative-serializer-minecraft:1.8.2-SNAPSHOT") } processResources { diff --git a/gradle.properties b/gradle.properties index ba0369b..c2aba36 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,8 +4,8 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.5 -yarn_mappings=1.21.5+build.1 +minecraft_version=1.21.6 +yarn_mappings=1.21.6+build.1 loader_version=0.16.14 loom_version=1.10-SNAPSHOT @@ -15,13 +15,16 @@ maven_group=lol.sylvie archives_base_name=bedframe # Dependencies -fabric_version=0.124.2+1.21.5 +fabric_version=0.127.0+1.21.6 # Find updates at https://maven.nucleoid.xyz/eu/pb4/polymer-core/ -polymer_version=0.12.6+1.21.5 +polymer_version=0.13.3+1.21.6 # Find updates at https://maven.nucleoid.xyz/xyz/nucleoid/server-translations-api/ -server_translations_version=2.5.0+1.21.5-rc1 +server_translations_version=2.5.1+1.21.5 # Find updates at https://repo.opencollab.dev/#/maven-snapshots/org/geysermc/geyser/api -geyser_version=2.7.1-SNAPSHOT \ No newline at end of file +geyser_version=2.8.0-SNAPSHOT + +# Find updates at https://repo.opencollab.dev/#/maven-snapshots/org/geysermc/pack/converter +pack_converter_version=3.3.2-SNAPSHOT \ No newline at end of file diff --git a/src/main/java/lol/sylvie/bedframe/BedframeInitializer.java b/src/main/java/lol/sylvie/bedframe/BedframeInitializer.java index 2d75394..ab5cdae 100644 --- a/src/main/java/lol/sylvie/bedframe/BedframeInitializer.java +++ b/src/main/java/lol/sylvie/bedframe/BedframeInitializer.java @@ -1,12 +1,16 @@ package lol.sylvie.bedframe; +import eu.pb4.polymer.resourcepack.api.PolymerResourcePackUtils; +import eu.pb4.polymer.resourcepack.api.ResourcePackBuilder; import lol.sylvie.bedframe.geyser.TranslationManager; +import lol.sylvie.bedframe.util.ResourceHelper; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.metadata.Person; import java.nio.file.Path; +import java.util.function.Consumer; import static lol.sylvie.bedframe.util.BedframeConstants.*; @@ -22,5 +26,9 @@ public void onInitialize() { TranslationManager manager = new TranslationManager(); manager.registerHooks(); }); + + PolymerResourcePackUtils.RESOURCE_PACK_AFTER_INITIAL_CREATION_EVENT.register(resourcePackBuilder -> { + ResourceHelper.PACK_BUILDER = resourcePackBuilder; + }); } } \ No newline at end of file diff --git a/src/main/java/lol/sylvie/bedframe/geyser/PackGenerator.java b/src/main/java/lol/sylvie/bedframe/geyser/PackGenerator.java index b6b6843..8cdb35b 100644 --- a/src/main/java/lol/sylvie/bedframe/geyser/PackGenerator.java +++ b/src/main/java/lol/sylvie/bedframe/geyser/PackGenerator.java @@ -3,12 +3,16 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import lol.sylvie.bedframe.util.BedframeConstants; -import lol.sylvie.bedframe.util.PathHelper; -import lol.sylvie.bedframe.util.ResourceHelper; -import lol.sylvie.bedframe.util.ZipHelper; +import lol.sylvie.bedframe.util.*; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.Version; +import net.minecraft.text.Text; +import net.minecraft.util.Pair; +import org.geysermc.pack.converter.util.NioDirectoryFileTreeReader; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackReader; +import xyz.nucleoid.server.translations.api.language.TranslationAccess; +import xyz.nucleoid.server.translations.impl.ServerTranslations; import java.io.File; import java.io.FileWriter; @@ -16,6 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.function.Function; import static lol.sylvie.bedframe.util.BedframeConstants.GSON; import static lol.sylvie.bedframe.util.BedframeConstants.METADATA; @@ -24,6 +29,8 @@ * Compiles the output of the {@link Translator} classes into a Bedrock resource pack */ public class PackGenerator { + private static final HashMap RESOURCE_PACK_MAP = new HashMap<>(); + private static JsonArray getVersionArray() { // TODO: A regex would be more inclusive Version version = BedframeConstants.METADATA.getVersion(); @@ -93,24 +100,23 @@ public void generatePack(Path packPath, File outputFile, List transl PathHelper.createDirectoryOrThrow(textsDir); // TODO: I'm not sure if translations are even necessary - /*JsonArray languages = new JsonArray(); + JsonArray languages = new JsonArray(); ArrayList> allKeys = new ArrayList<>(); translators.forEach(t -> allKeys.addAll(t.getTranslations())); TranslationHelper.LANGUAGES.forEach((code) -> { - TranslationAccess access = ServerTranslations.INSTANCE.getLanguage(code).serverTranslations(); try (FileWriter writer = new FileWriter(textsDir.resolve(code + ".lang").toFile())) { for (Pair keyPair : allKeys) { - writer.write(keyPair.getLeft() + "=" + access.get(keyPair.getRight()) + "\n"); + writer.write(keyPair.getLeft() + "=" + Text.translatable(keyPair.getRight()).getString() + "\n"); } } catch (IOException e) { - bedframe.getLogger().error("Couldn't write language file"); + BedframeConstants.LOGGER.error("Couldn't write language file"); } languages.add(code); }); - writeJsonToFile(languages, textsDir.resolve("languages.json").toFile());*/ + writeJsonToFile(languages, textsDir.resolve("languages.json").toFile()); Optional icon = METADATA.getIconPath(512); Files.copy(ResourceHelper.getResource(icon.orElseThrow()), packPath.resolve("pack_icon.png")); diff --git a/src/main/java/lol/sylvie/bedframe/geyser/TranslationManager.java b/src/main/java/lol/sylvie/bedframe/geyser/TranslationManager.java index 9897c9c..609b05d 100644 --- a/src/main/java/lol/sylvie/bedframe/geyser/TranslationManager.java +++ b/src/main/java/lol/sylvie/bedframe/geyser/TranslationManager.java @@ -59,6 +59,7 @@ public void registerHooks() { } event.register(ResourcePack.create(PackCodec.path(resourcePack))); + BedframeConstants.LOGGER.info("Registered resource pack"); } catch (IOException e) { BedframeConstants.LOGGER.error("Couldn't generate resource pack", e); } diff --git a/src/main/java/lol/sylvie/bedframe/geyser/Translator.java b/src/main/java/lol/sylvie/bedframe/geyser/Translator.java index 8fd4576..c1e5cd1 100644 --- a/src/main/java/lol/sylvie/bedframe/geyser/Translator.java +++ b/src/main/java/lol/sylvie/bedframe/geyser/Translator.java @@ -58,7 +58,7 @@ protected void writeOrThrow(FileWriter writer, String content) { } catch (IOException e) { throw new RuntimeException(e); } } - protected static void writeJsonToFile(JsonObject object, File file) { + protected static void writeJsonToFile(Object object, File file) { try (FileWriter writer = new FileWriter(file)) { GSON.toJson(object, writer); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/lol/sylvie/bedframe/geyser/model/JavaGeometryConverter.java b/src/main/java/lol/sylvie/bedframe/geyser/model/JavaGeometryConverter.java new file mode 100644 index 0000000..2cc5dd2 --- /dev/null +++ b/src/main/java/lol/sylvie/bedframe/geyser/model/JavaGeometryConverter.java @@ -0,0 +1,193 @@ +package lol.sylvie.bedframe.geyser.model; + +import net.kyori.adventure.key.Key; +import net.minecraft.util.Pair; +import org.geysermc.pack.bedrock.resource.models.entity.ModelEntity; +import org.geysermc.pack.bedrock.resource.models.entity.modelentity.Geometry; +import org.geysermc.pack.bedrock.resource.models.entity.modelentity.geometry.Bones; +import org.geysermc.pack.bedrock.resource.models.entity.modelentity.geometry.Description; +import org.geysermc.pack.bedrock.resource.models.entity.modelentity.geometry.bones.Cubes; +import org.geysermc.pack.bedrock.resource.models.entity.modelentity.geometry.bones.cubes.Uv; +import org.geysermc.pack.bedrock.resource.models.entity.modelentity.geometry.bones.cubes.uv.*; +import team.unnamed.creative.base.Axis3D; +import team.unnamed.creative.base.CubeFace; +import team.unnamed.creative.model.Element; +import team.unnamed.creative.model.ElementFace; +import team.unnamed.creative.model.ElementRotation; +import team.unnamed.creative.model.Model; +import team.unnamed.creative.texture.TextureUV; + +import java.util.*; + +import static lol.sylvie.bedframe.util.BedframeConstants.LOGGER; + +/* + * Concepts here were inspired by: + * tomalbrc's fork: https://github.com/tomalbrc/bedframe/blob/main/src/main/java/lol/sylvie/bedframe/geyser/translator/JavaToBedrockGeometryTranslator.java + * Pack Converter's ModelConverter: https://github.com/GeyserMC/PackConverter/blob/master/converter/src/main/java/org/geysermc/pack/converter/converter/model/ModelConverter.java#L62 + * (I'm not using ModelConverter directly as it requires some resource pack boilerplate we don't need) + */ +public class JavaGeometryConverter { + private static final String FORMAT_VERSION = "1.16.0"; + private static final String GEOMETRY_FORMAT = "geometry.%s"; + + private static float[] javaPosToBedrock(float[] java) { + return new float[] { java[0] - 8.0f, java[1], java[2] - 8.0f }; + } + + private static void applyFaceUv(Uv uv, CubeFace cubeFace, float[] uvValue, float[] uvSize, String texture) { + switch (cubeFace) { + case UP -> { + Up up = new Up(); + up.uv(uvValue); + up.uvSize(uvSize); + up.materialInstance(texture); + uv.up(up); + } + case DOWN -> { + Down down = new Down(); + down.uv(uvValue); + down.uvSize(uvSize); + down.materialInstance(texture); + uv.down(down); + } + case NORTH -> { + North north = new North(); + north.uv(uvValue); + north.uvSize(uvSize); + north.materialInstance(texture); + uv.north(north); + } + case SOUTH -> { + South south = new South(); + south.uv(uvValue); + south.uvSize(uvSize); + south.materialInstance(texture); + uv.south(south); + } + case EAST -> { + East east = new East(); + east.uv(uvValue); + east.uvSize(uvSize); + east.materialInstance(texture); + uv.east(east); + } + case WEST -> { + West west = new West(); + west.uv(uvValue); + west.uvSize(uvSize); + west.materialInstance(texture); + uv.west(west); + } + } + } + + public static Pair convert(Model model) { + List elements = model.elements(); + if (elements.isEmpty()) { + LOGGER.error("Model {} is empty :(", model.key()); + return null; + } + + ModelEntity modelEntity = new ModelEntity(); + modelEntity.formatVersion(FORMAT_VERSION); + + Geometry geometry = new Geometry(); + List bones = new ArrayList<>(); + + int nthElement = 0; + for (Element element : elements) { + float[] javaFrom = element.from().toArray(); + float[] javaTo = element.to().toArray(); + + // TODO: I've seen a lot of discussion over whether it should be one bone per cube or one bone for all cubes + Bones bone = new Bones(); + bone.name("element_" + nthElement); + + // Transform + Cubes cube = new Cubes(); + cube.origin(javaPosToBedrock(javaFrom)); + cube.size(new float[] { + javaTo[0] - javaFrom[0], + javaTo[1] - javaFrom[1], + javaTo[2] - javaFrom[2] }); + cube.inflate(0f); + + // Rotation + ElementRotation rotation = element.rotation(); + if (rotation != null) { // This can be null actually + float[] rotOrigin = rotation.origin().toArray(); + bone.pivot(javaPosToBedrock(rotOrigin)); + + // We are given an angle and an axis, and we need to provide a vector + float rotValue = (360 - rotation.angle()) % 360; + Axis3D axis = rotation.axis(); + float[] rotArray = new float[] { + axis == Axis3D.X ? rotValue : 0f, + axis == Axis3D.Y ? rotValue : 0f, + axis == Axis3D.Z ? rotValue : 0f + }; + bone.rotation(rotArray); + } else { + // this might be default, not sure + bone.pivot(new float[] { 0f, 0f, 0f }); + } + + // UV + Uv uv = new Uv(); + Map faceMap = element.faces(); + if (faceMap.isEmpty()) { + faceMap = new HashMap<>(); + for (CubeFace face : CubeFace.values()) { + faceMap.put(face, ElementFace.face().texture(face.name()).build()); + } + } + + for (Map.Entry faceEntry : faceMap.entrySet()) { + CubeFace direction = faceEntry.getKey(); + ElementFace face = faceEntry.getValue(); + + TextureUV textureUV = face.uv0(); + if (textureUV == null) + textureUV = TextureUV.uv(0, 0, 16f, 16f); + else textureUV = TextureUV.uv(textureUV.from().multiply(16f), textureUV.to().multiply(16f)); + + float[] uvValue; + float[] uvSize; + if (direction.axis() == Axis3D.Y) { + uvValue = new float[] { textureUV.to().x(), textureUV.to().y() }; + uvSize = new float[] { (textureUV.from().x() - uvValue[0]), (textureUV.from().y() - uvValue[1]) }; + } else { + uvValue = new float[] { textureUV.from().x(), textureUV.from().y() }; + uvSize = new float[] { (textureUV.to().x() - uvValue[0]), (textureUV.to().y() - uvValue[1]) }; + } + + applyFaceUv(uv, direction, uvValue, uvSize, face.texture().replace("#", "")); + } + + cube.uv(uv); + bone.cubes(List.of(cube)); + bone.textureMeshes(null); + bones.add(bone); + + nthElement++; + } + + geometry.bones(bones); + modelEntity.geometry(List.of(geometry)); + + String namespace = model.key().namespace(); + String[] pathSplit = model.key().value().split("/"); + String path = pathSplit[pathSplit.length - 1]; + String geometryName = String.format(GEOMETRY_FORMAT, (namespace.equals(Key.MINECRAFT_NAMESPACE) ? "" : namespace + ".") + + path.replace(":", ".")); + + Description description = new Description(); + description.identifier(geometryName); + description.textureWidth(16); + description.textureHeight(16); + geometry.description(description); + + return new Pair<>(geometryName, modelEntity); + } +} diff --git a/src/main/java/lol/sylvie/bedframe/geyser/translator/BlockTranslator.java b/src/main/java/lol/sylvie/bedframe/geyser/translator/BlockTranslator.java index fcc3b33..3b40fdd 100644 --- a/src/main/java/lol/sylvie/bedframe/geyser/translator/BlockTranslator.java +++ b/src/main/java/lol/sylvie/bedframe/geyser/translator/BlockTranslator.java @@ -4,23 +4,29 @@ import eu.pb4.polymer.blocks.api.BlockResourceCreator; import eu.pb4.polymer.blocks.api.PolymerBlockModel; import eu.pb4.polymer.blocks.api.PolymerTexturedBlock; +import eu.pb4.polymer.core.api.block.PolymerBlock; import lol.sylvie.bedframe.geyser.Translator; +import lol.sylvie.bedframe.geyser.model.JavaGeometryConverter; import lol.sylvie.bedframe.mixin.BlockResourceCreatorAccessor; import lol.sylvie.bedframe.mixin.PolymerBlockResourceUtilsAccessor; -import lol.sylvie.bedframe.util.JsonHelper; import lol.sylvie.bedframe.util.ResourceHelper; +import net.kyori.adventure.key.Key; import net.minecraft.block.Block; import net.minecraft.block.BlockState; +import net.minecraft.block.piston.PistonBehavior; import net.minecraft.command.argument.BlockArgumentParser; import net.minecraft.registry.Registries; +import net.minecraft.sound.BlockSoundGroup; import net.minecraft.state.property.BooleanProperty; import net.minecraft.state.property.EnumProperty; import net.minecraft.state.property.IntProperty; +import net.minecraft.state.property.Properties; import net.minecraft.state.property.Property; import net.minecraft.util.Identifier; import net.minecraft.util.Pair; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; import net.minecraft.util.math.Vec3d; import net.minecraft.util.shape.VoxelShape; import net.minecraft.world.EmptyBlockView; @@ -29,11 +35,21 @@ import org.geysermc.geyser.api.block.custom.CustomBlockState; import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; import org.geysermc.geyser.api.block.custom.component.*; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBoundingBox; import org.geysermc.geyser.api.event.EventBus; import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomBlocksEvent; import org.geysermc.geyser.api.util.CreativeCategory; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.geyser.util.SoundUtils; +import org.geysermc.pack.bedrock.resource.models.entity.ModelEntity; +import org.geysermc.pack.converter.converter.model.ModelStitcher; import org.joml.Vector3f; +import team.unnamed.creative.model.Model; +import team.unnamed.creative.model.ModelTexture; +import team.unnamed.creative.model.ModelTextures; +import team.unnamed.creative.serialize.minecraft.model.ModelSerializer; import xyz.nucleoid.packettweaker.PacketContext; import java.nio.file.Path; @@ -48,7 +64,6 @@ public class BlockTranslator extends Translator { // Maps parent models to a map containing the translations between Java sides and Bedrock sides private static final Map>> parentFaceMap = Map.of( "block/cube_all", List.of( - new Pair<>("particle", "*"), new Pair<>("all", "*") ), "block/cross", List.of( @@ -90,6 +105,7 @@ public class BlockTranslator extends Translator { ) ); + private static final ArrayList registeredBlocks = new ArrayList<>(); private final HashMap blocks = new HashMap<>(); public BlockTranslator() { @@ -128,21 +144,68 @@ private void populateProperties(CustomBlockData.Builder builder, Collection { Block realBlock = Registries.BLOCK.get(identifier); // Block names - addTranslationKey("tile." + identifier.toString() + ".name", realBlock.getTranslationKey()); + addTranslationKey("block." + identifier.getNamespace() + "." + identifier.getPath(), realBlock.getTranslationKey()); NonVanillaCustomBlockData.Builder builder = NonVanillaCustomBlockData.builder() .name(identifier.getPath()) @@ -176,10 +250,20 @@ public void handle(GeyserDefineCustomBlocksEvent event, Path packRoot) { for (BlockState state : realBlock.getStateManager().getStates()) { CustomBlockComponents.Builder stateComponentBuilder = CustomBlockComponents.builder(); + // Hardness + float hardness = state.getHardness(EmptyBlockView.INSTANCE, BlockPos.ORIGIN); + stateComponentBuilder.destructibleByMining(hardness); + // Obtain model data from polymers internal api BlockState polymerBlockState = block.getPolymerBlockState(state, PacketContext.get()); BlockResourceCreator creator = PolymerBlockResourceUtilsAccessor.getCREATOR(); PolymerBlockModel[] polymerBlockModels = ((BlockResourceCreatorAccessor) (Object) creator).getModels().get(polymerBlockState); + + if (polymerBlockModels == null || polymerBlockModels.length == 0) { + LOGGER.warn("No model specified for blockstate {}", state); + continue; + } + PolymerBlockModel modelEntry = polymerBlockModels[0]; // TODO: java selects one by weight, does bedrock support this? // Rotation @@ -187,53 +271,118 @@ public void handle(GeyserDefineCustomBlocksEvent event, Path packRoot) { stateComponentBuilder.transformation(rotationComponent); // Geometry - // TODO: More geometry types - JsonObject blockModel = ResourceHelper.readJsonResource(modelEntry.model().getNamespace(), "models/" + modelEntry.model().getPath() + ".json"); + String renderMethod = state.isOpaque() ? "opaque" : "blend"; // TODO: Hydraulic also faces this problem; Figure out when to use alpha_test + Identifier blockModelId = modelEntry.model(); + Model blockModel = resolveModel(blockModelId); if (blockModel == null) { LOGGER.warn("Couldn't load model for blockstate {}", state); continue; } - ModelData modelData = ModelData.fromJson(blockModel); - Identifier modelParent = modelData.parent(); - if (modelParent == null) { - LOGGER.error("Model for blockstate {} has no parent, defaulting to full block", state); - modelParent = Identifier.of("minecraft", "block/cube_all"); + // Textures + HashMap materials = new HashMap<>(); + Key modelParentKey = blockModel.parent(); + if (modelParentKey != null && parentFaceMap.containsKey(modelParentKey.value())) { + // Vanilla parent + boolean cross = modelParentKey.toString().equals("minecraft:block/cross"); + String geometryIdentifier = cross ? "minecraft:geometry.cross" : "minecraft:geometry.full_block"; + if (cross) renderMethod = "alpha_test_single_sided"; + + GeometryComponent geometryComponent = GeometryComponent.builder().identifier(geometryIdentifier).build(); + stateComponentBuilder.geometry(geometryComponent); + + ModelTextures textures = blockModel.textures(); + Map textureMap = textures.variables(); + List> faceMap = parentFaceMap.get(modelParentKey.value()); + + for (Pair face : faceMap) { + String javaFaceName = face.getLeft(); + String bedrockFaceName = face.getRight(); + if (!textureMap.containsKey(javaFaceName)) continue; + materials.put(bedrockFaceName, textureMap.get(javaFaceName)); + } + } else { + // Custom model + ModelStitcher.Provider provider = key -> resolveModel(Identifier.of(key.asString())); + blockModel = new ModelStitcher(provider, blockModel).stitch(); // This resolves parent models (?) + + Pair nameAndModel = JavaGeometryConverter.convert(blockModel); + if (nameAndModel == null) { + LOGGER.error("Couldn't convert model for blockstate {}", state); + continue; + } + String geometryId = nameAndModel.getLeft(); + writeJsonToFile(nameAndModel.getRight(), blockModelsDir.resolve(geometryId + ".geo.json").toFile()); + + for (Map.Entry entry : blockModel.textures().variables().entrySet()) { + String key = entry.getKey(); + ModelTexture texture = entry.getValue(); + materials.put(key, texture); + } + + GeometryComponent geometryComponent = GeometryComponent.builder().identifier(geometryId).build(); + stateComponentBuilder.geometry(geometryComponent); + } + + if (materials.isEmpty()) { + LOGGER.error("Couldn't generate materials for blockstate {}", state); + continue; + } + + // Particles + ModelTextures textures = blockModel.textures(); + if (!materials.containsKey("*")) { + ModelTexture texture = textures.particle() == null ? materials.values().iterator().next() : textures.particle(); + materials.put("*", texture); } - boolean cross = modelParent.toString().equals("minecraft:block/cross"); - String geometryIdentifier = cross ? "minecraft:geometry.cross" : "minecraft:geometry.full_block"; - String renderMethod = cross ? "alpha_test_single_sided" : "opaque"; - GeometryComponent geometryComponent = GeometryComponent.builder().identifier(geometryIdentifier).build(); - stateComponentBuilder.geometry(geometryComponent); + for (Map.Entry entry : materials.entrySet()) { + ModelTexture texture = entry.getValue(); - // Textures - List> faceMap = parentFaceMap.getOrDefault(modelParent.getPath(), parentFaceMap.get("block/cube_all")); - for (Pair face : faceMap) { - String javaFaceName = face.getLeft(); - String bedrockFaceName = face.getRight(); - if (!modelData.textures.containsKey(javaFaceName)) continue; - - String textureName = modelData.textures.get(javaFaceName); - Identifier textureIdentifier = Identifier.of(textureName); - String texturePath = "textures/" + textureIdentifier.getPath(); - String bedrockPath = ResourceHelper.javaToBedrockTexture(texturePath); - - JsonObject thisTexture = new JsonObject(); - thisTexture.addProperty("textures", bedrockPath); - textureDataObject.add(textureName, thisTexture); - - stateComponentBuilder.materialInstance(bedrockFaceName, MaterialInstance.builder() + while (texture.key() == null) { + String reference = texture.reference(); + if (reference == null || !materials.containsKey(reference)) { + break; + } + + texture = materials.get(reference); + } + + if (texture.key() == null) { + LOGGER.warn("Texture for block {} on side {} is missing", identifier, entry.getKey()); + continue; + } + + String textureName = texture.key().asString(); + if (!textureDataObject.has(textureName)) { + Identifier textureIdentifier = Identifier.of(textureName); + + String texturePath = "textures/" + textureIdentifier.getPath(); + String bedrockPath = ResourceHelper.javaToBedrockTexture(texturePath); + + JsonObject thisTexture = new JsonObject(); + thisTexture.addProperty("textures", bedrockPath); + textureDataObject.add(textureName, thisTexture); + + ResourceHelper.copyResource(textureIdentifier.getNamespace(), texturePath + ".png", packRoot.resolve(bedrockPath + ".png")); + } + + stateComponentBuilder.materialInstance(entry.getKey(), MaterialInstance.builder() .renderMethod(renderMethod) .texture(textureName) .faceDimming(true) - .ambientOcclusion(true) + .ambientOcclusion(blockModel.ambientOcclusion()) .build()); - - ResourceHelper.copyResource(textureIdentifier.getNamespace(), texturePath + ".png", packRoot.resolve(bedrockPath + ".png")); } - stateComponentBuilder.collisionBox(voxelShapeToBoxComponent(realBlock.getDefaultState().getCollisionShape(EmptyBlockView.INSTANCE, BlockPos.ORIGIN))); + // Collision + VoxelShape collisionBox = state.getCollisionShape(EmptyBlockView.INSTANCE, BlockPos.ORIGIN); + stateComponentBuilder.collisionBox(voxelShapeToBoxComponent(collisionBox)); + + VoxelShape outlineBox = state.getOutlineShape(EmptyBlockView.INSTANCE, BlockPos.ORIGIN); + stateComponentBuilder.selectionBox(voxelShapeToBoxComponent(outlineBox)); + + stateComponentBuilder.lightEmission(state.getLuminance()); CustomBlockComponents stateComponents = stateComponentBuilder.build(); if (state.getProperties().isEmpty()) { @@ -260,8 +409,44 @@ public void handle(GeyserDefineCustomBlocksEvent event, Path packRoot) { } builder.permutations(permutations); + // Sounds + // blocks.json + String blockAsString = identifier.toString(); + JsonObject thisBlockObject = new JsonObject(); + thisBlockObject.addProperty("sound", blockAsString); + blocksJson.add(blockAsString, thisBlockObject); + + // sounds.json + BlockSoundGroup soundGroup = realBlock.getDefaultState().getSoundGroup(); + // base sounds (break, hit, place) + JsonObject baseSoundObject = new JsonObject(); + baseSoundObject.addProperty("pitch", soundGroup.getPitch()); + baseSoundObject.addProperty("volume", soundGroup.getVolume()); + + JsonObject soundEventsObject = new JsonObject(); + soundEventsObject.addProperty("break", SoundUtils.translatePlaySound(soundGroup.getBreakSound().id().toString())); + soundEventsObject.addProperty("hit", SoundUtils.translatePlaySound(soundGroup.getHitSound().id().toString())); + soundEventsObject.addProperty("place", SoundUtils.translatePlaySound(soundGroup.getPlaceSound().id().toString())); + baseSoundObject.add("events", soundEventsObject); + + blockSoundsObject.add(blockAsString, baseSoundObject); + // interactive sounds + JsonObject interactiveSoundObject = new JsonObject(); + interactiveSoundObject.addProperty("pitch", soundGroup.getPitch()); + interactiveSoundObject.addProperty("volume", soundGroup.getVolume() * .4); // The multiplier is arbitrary, its just too loud by default :( + + JsonObject interactiveEventsObject = new JsonObject(); + interactiveEventsObject.addProperty("fall", SoundUtils.translatePlaySound(soundGroup.getFallSound().id().toString())); + interactiveEventsObject.addProperty("jump", SoundUtils.translatePlaySound(soundGroup.getStepSound().id().toString())); + interactiveEventsObject.addProperty("step", SoundUtils.translatePlaySound(soundGroup.getStepSound().id().toString())); + interactiveEventsObject.addProperty("land", SoundUtils.translatePlaySound(soundGroup.getFallSound().id().toString())); + interactiveSoundObject.add("events", interactiveEventsObject); + interactiveSoundsObject.add(blockAsString, interactiveSoundObject); + + // Registration NonVanillaCustomBlockData data = builder.build(); event.register(data); + registeredBlocks.add(block); // Registering the block states for (BlockState state : realBlock.getStateManager().getStates()) { @@ -281,12 +466,39 @@ public void handle(GeyserDefineCustomBlocksEvent event, Path packRoot) { } CustomBlockState customBlockState = stateBuilder.build(); - event.registerOverride(BlockArgumentParser.stringifyBlockState(block.getPolymerBlockState(state, PacketContext.get())), customBlockState); + JavaBlockState.Builder javaBlockState = JavaBlockState.builder(); + javaBlockState.blockHardness(state.getHardness(EmptyBlockView.INSTANCE, BlockPos.ORIGIN)); + + VoxelShape shape = state.getCollisionShape(EmptyBlockView.INSTANCE, BlockPos.ORIGIN); + if (shape.isEmpty()) { + javaBlockState.collision(new JavaBoundingBox[0]); + } else { + Box box = shape.getBoundingBox(); + javaBlockState.collision(new JavaBoundingBox[]{ + new JavaBoundingBox(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ) + }); + } + + javaBlockState.javaId(Block.getRawIdFromState(state)); + javaBlockState.identifier(BlockArgumentParser.stringifyBlockState(state)); + javaBlockState.waterlogged(state.get(Properties.WATERLOGGED, false)); + if (realBlock.asItem() != null) javaBlockState.pickItem(Registries.ITEM.getId(realBlock.asItem()).toString()); + javaBlockState.canBreakWithHand(state.isToolRequired()); + + PistonBehavior pistonBehavior = state.getPistonBehavior(); + javaBlockState.pistonBehavior(pistonBehavior == PistonBehavior.IGNORE ? "NORMAL" : pistonBehavior.name()); + + event.registerOverride(javaBlockState.build(), customBlockState); } }); terrainTextureObject.add("texture_data", textureDataObject); + soundsJson.add("block_sounds", blockSoundsObject); + interactiveSoundsWrapper.add("block_sounds", interactiveSoundsObject); + soundsJson.add("interactive_sounds", interactiveSoundsWrapper); writeJsonToFile(terrainTextureObject, textureDir.resolve("terrain_texture.json").toFile()); + writeJsonToFile(blocksJson, packRoot.resolve("blocks.json").toFile()); + writeJsonToFile(soundsJson, packRoot.resolve("sounds.json").toFile()); markResourcesProvided(); } @@ -294,10 +506,4 @@ public void handle(GeyserDefineCustomBlocksEvent event, Path packRoot) { public void register(EventBus eventBus, Path packRoot) { eventBus.subscribe(this, GeyserDefineCustomBlocksEvent.class, event -> handle(event, packRoot)); } - - record ModelData(Identifier parent, Map textures) { - public static ModelData fromJson(JsonObject object) { - return JsonHelper.GSON.fromJson(object, ModelData.class); - } - } } diff --git a/src/main/java/lol/sylvie/bedframe/geyser/translator/ItemTranslator.java b/src/main/java/lol/sylvie/bedframe/geyser/translator/ItemTranslator.java index 6313bf2..4f99e39 100644 --- a/src/main/java/lol/sylvie/bedframe/geyser/translator/ItemTranslator.java +++ b/src/main/java/lol/sylvie/bedframe/geyser/translator/ItemTranslator.java @@ -2,6 +2,8 @@ import com.google.gson.JsonObject; import eu.pb4.polymer.core.api.item.PolymerItem; +import eu.pb4.polymer.resourcepack.api.PolymerResourcePackUtils; +import eu.pb4.polymer.resourcepack.api.ResourcePackCreator; import lol.sylvie.bedframe.geyser.Translator; import lol.sylvie.bedframe.util.BedframeConstants; import lol.sylvie.bedframe.util.ResourceHelper; diff --git a/src/main/java/lol/sylvie/bedframe/mixin/CustomBlockRegistryPopulatorMixin.java b/src/main/java/lol/sylvie/bedframe/mixin/CustomBlockRegistryPopulatorMixin.java new file mode 100644 index 0000000..b187e6e --- /dev/null +++ b/src/main/java/lol/sylvie/bedframe/mixin/CustomBlockRegistryPopulatorMixin.java @@ -0,0 +1,28 @@ +package lol.sylvie.bedframe.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Map; + +@Mixin(value = CustomBlockRegistryPopulator.class, remap = false) +public class CustomBlockRegistryPopulatorMixin { + @ModifyVariable(method = "convertComponents", at = @At(value = "STORE", ordinal = 0)) + private static NbtMapBuilder addParticleComponent(NbtMapBuilder value, @Local(argsOnly = true) CustomBlockComponents components) { + Map materials = components.materialInstances(); + if (!materials.containsKey("*")) return value; + + value.putCompound("minecraft:destruction_particles", NbtMap.builder() + .putString("texture", materials.get("*").texture()) + .build()); + + return value; + } +} diff --git a/src/main/java/lol/sylvie/bedframe/mixin/PolymerBlockUtilsMixin.java b/src/main/java/lol/sylvie/bedframe/mixin/PolymerBlockUtilsMixin.java new file mode 100644 index 0000000..808016e --- /dev/null +++ b/src/main/java/lol/sylvie/bedframe/mixin/PolymerBlockUtilsMixin.java @@ -0,0 +1,23 @@ +package lol.sylvie.bedframe.mixin; + +import eu.pb4.polymer.core.api.block.PolymerBlock; +import eu.pb4.polymer.core.api.block.PolymerBlockUtils; +import lol.sylvie.bedframe.geyser.translator.BlockTranslator; +import lol.sylvie.bedframe.util.GeyserHelper; +import net.minecraft.block.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.CallbackInfoReturnable; +import xyz.nucleoid.packettweaker.PacketContext; + +@Mixin(PolymerBlockUtils.class) +public class PolymerBlockUtilsMixin { + // This mixin tells Polymer to send Bedrock clients + // the actual blocks rather than their Polymer representations + @Inject(method = "getBlockStateSafely(Leu/pb4/polymer/core/api/block/PolymerBlock;Lnet/minecraft/block/BlockState;ILxyz/nucleoid/packettweaker/PacketContext;)Lnet/minecraft/block/BlockState;", at = @At("RETURN"), cancellable = true) + private static void bedframe$tellPolymerToAbstain(PolymerBlock block, BlockState blockState, int maxDistance, PacketContext context, CallbackInfoReturnable cir) { + if (GeyserHelper.isBedrockPlayer(context.getPlayer()) && BlockTranslator.isRegisteredBlock(block)) + cir.setReturnValue(blockState); + } +} diff --git a/src/main/java/lol/sylvie/bedframe/util/BedframeConstants.java b/src/main/java/lol/sylvie/bedframe/util/BedframeConstants.java index efdd3ba..414c66b 100644 --- a/src/main/java/lol/sylvie/bedframe/util/BedframeConstants.java +++ b/src/main/java/lol/sylvie/bedframe/util/BedframeConstants.java @@ -5,6 +5,7 @@ import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.metadata.ModMetadata; import net.minecraft.util.Identifier; +import org.geysermc.pack.converter.util.LogListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/lol/sylvie/bedframe/util/ResourceHelper.java b/src/main/java/lol/sylvie/bedframe/util/ResourceHelper.java index a876ad2..9f5d8a1 100644 --- a/src/main/java/lol/sylvie/bedframe/util/ResourceHelper.java +++ b/src/main/java/lol/sylvie/bedframe/util/ResourceHelper.java @@ -1,8 +1,10 @@ package lol.sylvie.bedframe.util; import com.google.gson.JsonObject; +import eu.pb4.polymer.resourcepack.api.ResourcePackBuilder; import net.minecraft.util.Identifier; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -10,7 +12,14 @@ import java.nio.file.Path; public class ResourceHelper { + public static ResourcePackBuilder PACK_BUILDER = null; + public static InputStream getResource(String path) { + if (PACK_BUILDER != null) { + byte[] data = PACK_BUILDER.getData(path); + if (data != null) return new ByteArrayInputStream(data); + } + return Thread.currentThread().getContextClassLoader().getResourceAsStream(path); } diff --git a/src/main/java/lol/sylvie/bedframe/util/TranslationHelper.java b/src/main/java/lol/sylvie/bedframe/util/TranslationHelper.java index aecd56c..120a18a 100644 --- a/src/main/java/lol/sylvie/bedframe/util/TranslationHelper.java +++ b/src/main/java/lol/sylvie/bedframe/util/TranslationHelper.java @@ -7,12 +7,14 @@ public class TranslationHelper { public static ArrayList LANGUAGES = new ArrayList<>(); static { + /* for (ServerLanguageDefinition language : ServerLanguageDefinition.getAllLanguages()) { String code = language.code(); String[] sides = code.split("_"); if (sides.length == 2) { LANGUAGES.add(sides[0] + "_" + sides[1].toUpperCase()); } else LANGUAGES.add(code); - } + }*/ + LANGUAGES.add("en_US"); } } diff --git a/src/main/resources/bedframe.mixins.json b/src/main/resources/bedframe.mixins.json index 18f4e6f..e18d717 100644 --- a/src/main/resources/bedframe.mixins.json +++ b/src/main/resources/bedframe.mixins.json @@ -4,8 +4,10 @@ "compatibilityLevel": "JAVA_21", "mixins": [ "BlockResourceCreatorAccessor", + "CustomBlockRegistryPopulatorMixin", "CustomItemRegistryPopulatorMixin", "PolymerBlockResourceUtilsAccessor", + "PolymerBlockUtilsMixin", "PolymerItemMixin", "PolymerItemUtilsMixin" ], diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 568171f..c1f26d3 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,7 +25,7 @@ ], "depends": { "fabricloader": ">=0.16.14", - "minecraft": "~1.21.5", + "minecraft": ["1.21.6", "1.21.7"], "java": ">=21", "fabric-api": "*", "geyser-fabric": "*", diff --git a/src/testmod/java/lol/sylvie/testmod/block/ModBlocks.java b/src/testmod/java/lol/sylvie/testmod/block/ModBlocks.java index 04c7442..6d98148 100644 --- a/src/testmod/java/lol/sylvie/testmod/block/ModBlocks.java +++ b/src/testmod/java/lol/sylvie/testmod/block/ModBlocks.java @@ -3,6 +3,7 @@ import eu.pb4.polymer.core.api.item.PolymerBlockItem; import lol.sylvie.testmod.Testmod; import lol.sylvie.testmod.block.impl.TexturedExampleBlock; +import lol.sylvie.testmod.block.impl.TexturedFlowerPotExampleBlock; import lol.sylvie.testmod.block.impl.TexturedFlowerExampleBlock; import lol.sylvie.testmod.block.impl.TexturedLogExampleBlock; import net.minecraft.block.AbstractBlock; @@ -40,6 +41,13 @@ public class ModBlocks { Items.WITHER_ROSE ); + public static final Block EXAMPLE_FLOWER_POT = register( + "example_flower_pot", + TexturedFlowerPotExampleBlock::new, + AbstractBlock.Settings.create().sounds(BlockSoundGroup.FLOWERBED).breakInstantly().nonOpaque(), + Items.FLOWER_POT + ); + private static Block register(String name, Function blockFactory, AbstractBlock.Settings settings, Item polymerItem) { RegistryKey blockKey = keyOfBlock(name); Block block = blockFactory.apply(settings.registryKey(blockKey)); diff --git a/src/testmod/java/lol/sylvie/testmod/block/impl/TexturedFlowerPotExampleBlock.java b/src/testmod/java/lol/sylvie/testmod/block/impl/TexturedFlowerPotExampleBlock.java new file mode 100644 index 0000000..8b80b9b --- /dev/null +++ b/src/testmod/java/lol/sylvie/testmod/block/impl/TexturedFlowerPotExampleBlock.java @@ -0,0 +1,11 @@ +package lol.sylvie.testmod.block.impl; + +import eu.pb4.polymer.blocks.api.BlockModelType; +import lol.sylvie.testmod.Testmod; +import net.minecraft.util.Identifier; + +public class TexturedFlowerPotExampleBlock extends SimplePolymerTexturedBlock { + public TexturedFlowerPotExampleBlock(Settings settings) { + super(settings, BlockModelType.TRANSPARENT_BLOCK, Identifier.of(Testmod.MOD_ID, "block/example_flower_pot")); + } +} diff --git a/src/testmod/java/lol/sylvie/testmod/item/ModItems.java b/src/testmod/java/lol/sylvie/testmod/item/ModItems.java index 540e4ef..a61dfb5 100644 --- a/src/testmod/java/lol/sylvie/testmod/item/ModItems.java +++ b/src/testmod/java/lol/sylvie/testmod/item/ModItems.java @@ -57,6 +57,7 @@ public static void initialize() { itemGroup.add(ModBlocks.EXAMPLE_BLOCK.asItem()); itemGroup.add(ModBlocks.EXAMPLE_LOG.asItem()); itemGroup.add(ModBlocks.EXAMPLE_FLOWER.asItem()); + itemGroup.add(ModBlocks.EXAMPLE_FLOWER_POT.asItem()); }); } } \ No newline at end of file diff --git a/src/testmod/resources/assets/bedframe-testmod/blockstates/example_flower_pot.json b/src/testmod/resources/assets/bedframe-testmod/blockstates/example_flower_pot.json new file mode 100644 index 0000000..fbf555c --- /dev/null +++ b/src/testmod/resources/assets/bedframe-testmod/blockstates/example_flower_pot.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "bedframe-testmod:block/example_flower_pot" + } + } +} \ No newline at end of file diff --git a/src/testmod/resources/assets/bedframe-testmod/items/example_flower_pot.json b/src/testmod/resources/assets/bedframe-testmod/items/example_flower_pot.json new file mode 100644 index 0000000..4b806e2 --- /dev/null +++ b/src/testmod/resources/assets/bedframe-testmod/items/example_flower_pot.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "bedframe-testmod:block/example_flower_pot" + } +} \ No newline at end of file diff --git a/src/testmod/resources/assets/bedframe-testmod/models/block/example_flower_pot.json b/src/testmod/resources/assets/bedframe-testmod/models/block/example_flower_pot.json new file mode 100644 index 0000000..b8a8fa4 --- /dev/null +++ b/src/testmod/resources/assets/bedframe-testmod/models/block/example_flower_pot.json @@ -0,0 +1,90 @@ +{ + "credit": "Made with Blockbench", + "ambientocclusion": false, + "texture_size": [32, 32], + "textures": { + "3": "bedframe-testmod:block/example_flower_pot" + }, + "elements": [ + { + "from": [5, 0, 5], + "to": [6, 6, 11], + "faces": { + "north": {"uv": [4, 6, 4.5, 9], "texture": "#3"}, + "east": {"uv": [5.5, 0, 8.5, 3], "texture": "#3"}, + "south": {"uv": [4.5, 6, 5, 9], "texture": "#3"}, + "west": {"uv": [5.5, 0, 8.5, 3], "texture": "#3"}, + "up": {"uv": [5.5, 9, 5, 6], "texture": "#3"}, + "down": {"uv": [5.5, 6, 5, 9], "texture": "#3", "cullface": "down"} + } + }, + { + "from": [10, 0, 5], + "to": [11, 6, 11], + "faces": { + "north": {"uv": [4.5, 6, 5, 9], "texture": "#3"}, + "east": {"uv": [5.5, 0, 8.5, 3], "texture": "#3"}, + "south": {"uv": [4, 6, 4.5, 9], "texture": "#3"}, + "west": {"uv": [5.5, 0, 8.5, 3], "texture": "#3"}, + "up": {"uv": [6, 9, 5.5, 6], "texture": "#3"}, + "down": {"uv": [6, 6, 5.5, 9], "texture": "#3", "cullface": "down"} + } + }, + { + "from": [6, 0, 5], + "to": [10, 6, 6], + "faces": { + "north": {"uv": [5.5, 3, 7.5, 6], "texture": "#3"}, + "south": {"uv": [5.5, 3, 7.5, 6], "texture": "#3"}, + "up": {"uv": [8, 6.5, 6, 6], "texture": "#3"}, + "down": {"uv": [8, 6.5, 6, 7], "texture": "#3", "cullface": "down"} + } + }, + { + "from": [6, 0, 10], + "to": [10, 6, 11], + "faces": { + "north": {"uv": [5.5, 3, 7.5, 6], "texture": "#3"}, + "south": {"uv": [5.5, 3, 7.5, 6], "texture": "#3"}, + "up": {"uv": [8, 7, 6, 6.5], "texture": "#3"}, + "down": {"uv": [8, 6, 6, 6.5], "texture": "#3", "cullface": "down"} + } + }, + { + "from": [6, 0, 6], + "to": [10, 4, 10], + "faces": { + "up": {"uv": [2, 8, 0, 6], "texture": "#3"}, + "down": {"uv": [4, 6, 2, 8], "texture": "#3", "cullface": "down"} + } + }, + { + "from": [2.6, 4, 8], + "to": [13.4, 16, 8], + "shade": false, + "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8], "rescale": true}, + "faces": { + "north": {"uv": [0, 0, 5.5, 6], "texture": "#3"}, + "south": {"uv": [0, 0, 5.5, 6], "texture": "#3"} + } + }, + { + "from": [8, 4, 2.6], + "to": [8, 16, 13.4], + "shade": false, + "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8], "rescale": true}, + "faces": { + "east": {"uv": [0, 0, 5.5, 6], "texture": "#3"}, + "west": {"uv": [0, 0, 5.5, 6], "texture": "#3"} + } + } + ], + "groups": [ + { + "name": "flower_pot_cross", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2, 3, 4, 5, 6] + } + ] +} \ No newline at end of file diff --git a/src/testmod/resources/assets/bedframe-testmod/textures/block/example_flower_pot.png b/src/testmod/resources/assets/bedframe-testmod/textures/block/example_flower_pot.png new file mode 100644 index 0000000..addf7de Binary files /dev/null and b/src/testmod/resources/assets/bedframe-testmod/textures/block/example_flower_pot.png differ diff --git a/src/testmod/resources/data/bedframe-testmod/lang/en_us.json b/src/testmod/resources/data/bedframe-testmod/lang/en_us.json index 6c2278a..0bb63d1 100644 --- a/src/testmod/resources/data/bedframe-testmod/lang/en_us.json +++ b/src/testmod/resources/data/bedframe-testmod/lang/en_us.json @@ -4,5 +4,6 @@ "item.bedframe-testmod.vanilla_textured": "Vanilla-Textured Item", "block.bedframe-testmod.example_block": "Example Block", "block.bedframe-testmod.example_log": "Example Log", - "block.bedframe-testmod.example_flower": "Example Flower" + "block.bedframe-testmod.example_flower": "Example Flower", + "block.bedframe-testmod.example_flower_pot": "Example Flower Pot" } \ No newline at end of file