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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ jobs:
if-no-files-found: error
retention-days: 30
path: |
build/libs/SkysBuildingPieces-0.3.9.121012.jar
build/libs/SkysBuildingPieces-0.3.9.121012-sources.jar
build/libs/SkysBuildingPieces-0.3.9.121012-javadoc.jar
build/libs/SkysBuildingPieces-0.3.10.121012.jar
build/libs/SkysBuildingPieces-0.3.10.121012-sources.jar
build/libs/SkysBuildingPieces-0.3.10.121012-javadoc.jar
build/release/SHA256SUMS
CHANGELOG.md

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 0.3.10.121012

- Give each crafting result a stable recipe-book button, with alternative ways to make that item grouped together.
- Cover exposed grass beneath snow pieces and snowy steps or corners, restoring its normal appearance when they are removed or thaw.
- Unlock shared Grass Slabs recipes when crafting their ingredients, including pieces still held on the cursor.
- Remove repeated recipe-book entries while keeping each cut and rebuilding pattern.
- Keep dirt vertical slabs craftable back into dirt when Grass Slabs isn't installed.
- Let snow pieces drop themselves when broken by hand, as well as with a shovel.
- Keep grass spreading within the world's current height limits, including below zero and above 255.
- Make standalone grass decay under solid cover or water even in bright light.
- Scale wooden pieces' furnace fuel to their size and the original wood's fuel value.
- Keep grass steps and corners snowy beside a higher snow-covered terrace.
- Give exposed grass pieces a snow cap in cold areas where layers cannot settle.
- Refresh grass-piece appearance and growth during normal random ticks.
- Recognise snow pieces as snow cover alongside vanilla snow.

## 0.3.9.121012

- Give each material and shape its own block ID. Older worlds keep their pieces and their orientation.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,25 @@ Pieces support native waterlogging. Wet grass and path pieces turn into their
matching dirt shapes, keeping the water and orientation. Drain them before
using a shovel.

Snow pieces drop themselves when dug by hand or with a shovel. Wooden slabs,
steps and corners burn for the corresponding share of their original block's
furnace fuel, so cutting wood does not create extra fuel.

Snow pieces and snowy grass steps or corners cover the exposed top of a grass
block beneath them. This is a visual covering: the grass stays intact and the
walking surface keeps its height. Remove or thaw the piece to reveal the grass.

## Crafting

Recipes appear in the recipe book as you collect their ingredients. Crafting a
piece also reveals its next cuts and recombination recipes while the table is
still open.

Each button shows one result. Right-click it to choose between different ways
of making that same piece. Different materials and shapes stay separate.
For snow slabs, put two snow blocks diagonally in either crafting grid to make
four slabs. Vanilla's three-block row still makes snow layers.

Craft pieces from matching full blocks, slabs and steps. For example, three
full blocks in a row make six slabs, and two matching slabs placed diagonally
make four steps. Existing vanilla recipes stay available.
Expand Down
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,12 @@ def verifyEclipseConfiguration = tasks.register('verifyEclipseConfiguration') {
apply from: 'gradle/native-unit-launcher.gradle'
apply from: 'gradle/optional-runs.gradle'
apply from: 'gradle/native-probes.gradle'

apply from: 'gradle/recipe-resource-sync.gradle'

tasks.register('verifyGeneratedRecipeOwnership', Exec) {
group = 'verification'
description = 'Checks standalone cuts and ownership of shared soil recipes.'
commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), 'tools/test_recipe_ownership.py'
}
tasks.named('check') { dependsOn tasks.named('verifyGeneratedRecipeOwnership') }
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ release_enabled=true
curseforge_project_id=1754162
mod_name=Sky's Building Pieces
mod_license=LGPL-2.1-only
mod_version=0.3.9.121012
mod_version=0.3.10.121012
mod_group=zone.moddev.mc.skysbuildingpieces
mod_authors=SkyBlade1978
mod_description=Extra building shapes for vanilla materials, with built-in Mineralogy support when installed. No world generation.
12 changes: 6 additions & 6 deletions gradle/grass-slabs-release.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Coordinated Grass Slabs candidate for NeoForge 1.21.1.
kind=local-candidate
version=1.2.0.121012
coordinate=zone.moddev.mc.skysgrassslabs:SkysGrassSlabs:1.2.0.121012
sha256=F095DD84AF9BFB6F90A9C3A091B526508C9516D3B76E0162ED67B470004322F8
development.sha256=F095DD84AF9BFB6F90A9C3A091B526508C9516D3B76E0162ED67B470004322F8
source.commit=6061fa264f3d4e710fde7279ffe6d3958fc98929
kind=release
version=1.2.1.121012
coordinate=zone.moddev.mc.skysgrassslabs:SkysGrassSlabs:1.2.1.121012
sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8
development.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8
source.commit=dfc69bddd7f9aa9a8ddde74673d3e5b19286b763
65 changes: 65 additions & 0 deletions gradle/recipe-resource-sync.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Keep removed recipes out of packaged resources and existing Eclipse outputs.
def recipeOwners = project.mod_id == 'skysbuildingpiecesbop'
? ['skysbuildingpieces', 'skysbuildingpiecesbop'] : [project.mod_id]
def recipeFolders = recipeOwners.collectMany { owner ->
["data/${owner}/recipe", "data/${owner}/advancement/recipes"]
}

def insideProject = { File directory ->
File resolved = directory.canonicalFile
if (!resolved.toPath().startsWith(projectDir.canonicalFile.toPath())
|| resolved == projectDir.canonicalFile) {
throw new GradleException("Recipe output is outside this checkout: ${resolved}")
}
resolved
}

def recipeFiles = { File root ->
Set<String> files = new TreeSet<>()
recipeFolders.each { relative ->
File folder = new File(root, relative)
fileTree(folder) { include '**/*.json' }.files.each { path ->
files.add(relative + '/' + folder.toPath().relativize(path.toPath()).toString().replace('\\', '/'))
}
}
files
}

tasks.named('processResources') {
doFirst {
File output = insideProject(destinationDir)
recipeFolders.each { relative ->
project.delete(insideProject(new File(output, relative)))
}
}
}

tasks.named('prepareEclipseResources') {
doLast {
File eclipseOutput = insideProject(file('bin/main'))
recipeFolders.each { relative ->
project.sync {
from(layout.buildDirectory.dir("resources/main/${relative}"))
into(insideProject(new File(eclipseOutput, relative)))
}
}
}
}

def verifyRecipeResourceOutputs = tasks.register('verifyRecipeResourceOutputs') {
group = 'verification'
description = 'Checks that processed and Eclipse recipes match the current source resources.'
dependsOn tasks.named('processResources'), tasks.named('prepareEclipseResources')
doLast {
Set<String> expected = new TreeSet<>()
sourceSets.main.resources.srcDirs.each { root -> expected.addAll(recipeFiles(root)) }
[layout.buildDirectory.dir('resources/main').get().asFile, file('bin/main')].each { output ->
Set<String> actual = recipeFiles(output)
if (actual != expected) {
throw new GradleException("Recipe output mismatch in ${output}: "
+ "missing=${expected - actual}, stale=${actual - expected}")
}
}
}
}
tasks.named('check') { dependsOn verifyRecipeResourceOutputs }
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ public void tick(net.neoforged.neoforge.client.event.ClientTickEvent.Post event)
}
if (startedWorld && ClientStatus.complete && minecraft.level != null && minecraft.getConnection() != null
&& !minecraft.getConnection().getRecipeManager().getRecipes().isEmpty()) {
RecipeBookGroupChecks.run(minecraft);
RecipePreviewModels.check(minecraft);
colours(minecraft);
SnowCoverClientChecks.run(minecraft);
net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(minecraft.level.enabledFeatures(),
true, minecraft.level.registryAccess());
for (var entry : PiecesTab.entries().values()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package zone.moddev.mc.skysbuildingpieces.probe;

import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeType;
import zone.moddev.mc.skysbuildingpieces.content.PieceItem;
import zone.moddev.mc.skysbuildingpieces.content.Pieces;

/** Cutting and recombining wood must not multiply its furnace fuel value. */
public final class FuelChecks {
public static void run() {
int cases = 0;
for (var block : Pieces.playableBlocks()) {
var definition = block.definition(block.defaultBlockState());
if (!definition.type.equals("wood")) {
continue;
}
PieceItem item = (PieceItem) block.asItem();
ItemStack full = Pieces.stack(Pieces.nativeState(definition, "full"), 1);
int divisor = switch (block.palette.shape) {
case SLAB, VERTICAL_SLAB -> 2;
case STEP, HORIZONTAL_STEP, VERTICAL_STEP -> 4;
case CORNER -> 8;
default -> 1;
};
for (var furnace : java.util.List.of(RecipeType.SMELTING, RecipeType.SMOKING, RecipeType.BLASTING)) {
int original = Math.max(0, full.getBurnTime(furnace));
int actual = new ItemStack(item).getBurnTime(furnace);
if (actual != original / divisor) {
throw new AssertionError("Wood fuel changed through cutting: " + definition.id + "/"
+ block.palette.shape + " expected=" + original / divisor + " actual=" + actual);
}
cases++;
}
}
org.apache.logging.log4j.LogManager.getLogger().info("BUILDING_PIECES_FUEL_PASS cases=" + cases);
}

private FuelChecks() {
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -491,14 +491,20 @@ private static void verifyHarvesting(ServerLevel world, BlockPos position, Serve
for (Item tool : new Item[] {Items.WOODEN_PICKAXE, Items.STONE_PICKAXE,
Items.IRON_PICKAXE, Items.DIAMOND_PICKAXE, Items.IRON_AXE, Items.IRON_SHOVEL}) {
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(tool));
require(piece.canHarvestBlock(world, position, player) == full.canHarvestBlock(world, position, player),
boolean snowPiece = piece.getBlock() instanceof PieceBlock block
&& block.definition(piece).type.equals("snow");
require(piece.canHarvestBlock(world, position, player)
== (snowPiece || full.canHarvestBlock(world, position, player)),
"Material harvest requirements changed: " + piece + " with " + tool);
float pieceHardness = piece.getDestroySpeed(world, position);
float fullHardness = full.getDestroySpeed(world, position);
float expected = full.getDestroyProgress(player, world, position);
if (pieceHardness >= 0 && fullHardness > 0) {
expected = expected * fullHardness / pieceHardness;
}
if (snowPiece) {
expected = player.getDigSpeed(full, position) / pieceHardness / 30;
}
float actual = piece.getDestroyProgress(player, world, position);
require(Float.compare(actual, expected) == 0 || Math.abs(actual - expected) < 0.00001F,
"Material mining progress changed: " + piece + " with " + tool);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public final class RecipeBookChecks {
"skysgrassslabs", "skysterrainsmoother", "skysterrainsmootherbop"));

public static void run(ServerLevel world) {
RecipeUniquenessChecks.run(world);
SortedMap<String, JsonObject> entries = new TreeMap<>();
List<RecipeHolder<?>> recipes = new ArrayList<>();

Expand Down Expand Up @@ -127,11 +128,7 @@ public void send(Packet<?> packet,
throw new IllegalStateException(failure);
}
for (RecipeHolder<?> holder : recipes) {
if (Boolean.getBoolean("skysbuildingpieces.checkAllSkyRecipes")
|| holder.id().getNamespace().equals("skysbuildingpieces")
|| holder.id().getNamespace().equals("skysbuildingpiecesbop")) {
require(player.getRecipeBook().contains(holder), "Saved recipe book lost " + holder.id());
}
require(player.getRecipeBook().contains(holder), "Saved recipe book lost " + holder.id());
}
}

Expand All @@ -142,12 +139,6 @@ public void send(Packet<?> packet,
ResourceLocation id = holder.id();
Recipe<?> recipe = holder.value();

if (!Boolean.getBoolean("skysbuildingpieces.checkAllSkyRecipes")
&& !id.getNamespace().equals("skysbuildingpieces")
&& !id.getNamespace().equals("skysbuildingpiecesbop")) {
continue;
}

verified.add(holder);

AdvancementHolder advancement = world.getServer().getAdvancements()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package zone.moddev.mc.skysbuildingpieces.probe;

import java.util.List;
import java.util.Set;
import net.minecraft.client.ClientRecipeBook;
import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.StackedContents;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.CraftingInput;
import net.minecraft.world.item.crafting.RecipeType;

/** Check the collections used by recipe-book buttons, not just individual recipe JSON. */
public final class RecipeBookGroupChecks {
private static final Set<String> SKY = Set.of("skysgrassslabs", "skysbuildingpieces",
"skysbuildingpiecesbop", "skysterrainsmoother", "skysterrainsmootherbop");

public static void run(Minecraft client) {
int checked = 0;
for (var collection : client.player.getRecipeBook().getCollections()) {
if (collection.getRecipes().stream().anyMatch(holder -> SKY.contains(holder.id().getNamespace()))) {
require(collection.hasSingleResultItem(),
"A Sky recipe-book button must not cycle between different output items: "
+ collection.getRecipes().stream().limit(5).map(holder -> holder.id().toString()).toList());
checked++;
}
}
require(checked > 0, "Sky recipes reached the client book collections");

var recipes = client.getConnection().getRecipeManager();
var access = client.level.registryAccess();
var slab = BuiltInRegistries.ITEM.get(ResourceLocation.parse("skysbuildingpieces:snow_slab"));
CraftingInput diagonal = CraftingInput.of(2, 2, List.of(new ItemStack(Items.SNOW_BLOCK),
ItemStack.EMPTY, ItemStack.EMPTY, new ItemStack(Items.SNOW_BLOCK)));
var matches = recipes.getRecipesFor(RecipeType.CRAFTING, diagonal, client.level);
require(matches.size() == 1, "Two diagonal snow blocks have one crafting result");
ItemStack result = matches.getFirst().value().assemble(diagonal, access);
require(result.is(slab) && result.getCount() == 4, "Two snow blocks make four snow slabs");

ClientRecipeBook book = new ClientRecipeBook();
book.setupCollections(recipes.getRecipes(), access);
for (var holder : recipes.getRecipes()) {
if (holder.value().getResultItem(access).is(slab)) {
book.add(holder);
}
}
StackedContents inventory = new StackedContents();
inventory.accountStack(new ItemStack(Items.SNOW_BLOCK, 2));
var buttons = book.getCollections().stream().filter(collection -> collection.getRecipes().stream()
.anyMatch(holder -> holder.value().getResultItem(access).is(slab))).toList();
require(buttons.size() == 1, "Snow slabs have one stable result button");
var button = buttons.getFirst();
button.updateKnownRecipes(book);
button.canCraft(inventory, 2, 2, book);
require(button.hasSingleResultItem() && button.hasKnownRecipes() && button.hasCraftable()
&& !button.getDisplayRecipes(true).isEmpty(),
"The craftable-only 2x2 recipe book shows the snow slab recipe");
org.apache.logging.log4j.LogManager.getLogger().info(
"SKY_RECIPE_BOOK_GROUPS_PASS buttons=" + checked
+ " distinct_results=true snow_slab_visible=true snow_slab_crafting=true");
}

private static void require(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}

private RecipeBookGroupChecks() {
}
}
Loading
Loading