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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ jobs:
if-no-files-found: error
retention-days: 30
path: |
build/libs/SkysGrassSlabs-1.1.1.112021.jar
build/libs/SkysGrassSlabs-1.1.1.112021-sources.jar
build/libs/SkysGrassSlabs-1.1.1.112021-javadoc.jar
build/libs/SkysGrassSlabs-1.1.2.112021.jar
build/libs/SkysGrassSlabs-1.1.2.112021-sources.jar
build/libs/SkysGrassSlabs-1.1.2.112021-javadoc.jar
build/release/SHA256SUMS
CHANGELOG.md

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

## 1.1.2.112021

- Added the missing recipe-book unlocks for crafting and rebuilding pieces.
- Made the next recipes appear while the crafting table is still open.

## 1.1.1.112021 - Minecraft 1.12.2

- Added a Config screen with help for each setting. Save your changes and
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -1393,3 +1393,5 @@ tasks.register('verifyEclipseProductionClasspath') {
tasks.named('check') {
dependsOn tasks.named('verifyReleaseMetadata')
}

apply from: 'gradle/recipe-advancements.gradle'
11 changes: 11 additions & 0 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,14 @@ those allowances. Unrelated warnings remain visible in the game logs.
Cross-version fixtures have separate, reviewed diagnostics tied to their archive
checksums. These cover the test-only generator mod and exact vanilla numeric-ID
changes. They are not applied to ordinary worlds or used to silence the game.

## Recipe-book resources

The build generates unlock advancements from `src/main/recipe-book/recipes.json`.
The runtime checks compare those entries with the registered recipes and exercise
inventory discovery, crafting-table discovery and recipe-book persistence.
Optional catalogue entries load only when their recipe exists.

Recipe unlocks have no shared advancement parent. Minecraft 1.12 walks that
visibility tree recursively when loading completed progress, which can overflow
with a large catalogue. The inventory and crafting unlock criteria are unchanged.
6 changes: 6 additions & 0 deletions docs/RELEASE-1.1.2.112021.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Recipe book update for Minecraft 1.12.2

Recipes now appear in the recipe book when you obtain their ingredients.
Crafting a piece also reveals its next cuts and recombination recipes while
the crafting table is open. Saved blocks, items and crafting quantities are
unchanged.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ curseforge_project_id=1677588
mod_id=skysgrassslabs
mod_name=Sky's Grass Slabs
mod_license=LGPL-2.1-only
mod_version=1.1.1.112021
mod_version=1.1.2.112021
mod_group=zone.moddev.mc.skysgrassslabs
mod_authors=SkyBlade1978
mod_description=Adds dirt, grass and path slabs, turf, natural grass spreading and smoother Overworld terrain.
46 changes: 46 additions & 0 deletions gradle/recipe-advancements.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// Recipe IDs and exact ingredient variants are checked against the live registry.
def recipeBookManifest = layout.projectDirectory.file('src/main/recipe-book/recipes.json')
def recipeBookOutput = layout.buildDirectory.dir('generated/recipe-advancements')
def generateRecipeAdvancements = tasks.register('generateRecipeAdvancements') {
inputs.file(recipeBookManifest)
outputs.dir(recipeBookOutput)
doLast {
File output = recipeBookOutput.get().asFile
delete output
List entries = new JsonSlurper().parse(recipeBookManifest.asFile) as List
Set<String> ids = new HashSet<>()
entries.each { entry ->
String recipe = entry.recipe
String owner = project.mod_id
if (entry.owner != owner || !ids.add(recipe) || entry.ingredients.isEmpty()) {
throw new GradleException("Invalid recipe-book entry: ${recipe}")
}
String path = recipe.split(':', 2)[1]
Map criteria = [:]
List<String> requirements = []
entry.ingredients.eachWithIndex { ingredient, index ->
String key = entry.ingredients.size() == 1 ? 'has_ingredient' : 'has_ingredient_' + index
criteria[key] = [trigger: 'minecraft:inventory_changed', conditions: [items: [ingredient]]]
requirements.add(key)
}
criteria.has_the_recipe = [trigger: 'minecraft:recipe_unlocked', conditions: [recipe: recipe]]
requirements.add('has_the_recipe')
// A shared parent makes 1.12 recurse through thousands of learned recipes on login.
// Independent hidden unlocks keep the same recipe-book behaviour without that recursion.
Map advancement = [rewards: [recipes: [recipe]],
criteria: criteria, requirements: [requirements]]
if (project.mod_id != 'skysgrassslabs') {
// Optional catalogues and recovery recipes must not leave phantom unlocks.
advancement.conditions = [[type: 'skysbuildingpieces:recipe_exists', recipe: recipe]]
}
File target = new File(output, "assets/${owner}/advancements/recipes/${path}.json")
target.parentFile.mkdirs()
target.setText(JsonOutput.prettyPrint(JsonOutput.toJson(advancement)) + '\n', 'UTF-8')
}
}
}
sourceSets.main.resources.srcDir(generateRecipeAdvancements)
tasks.named('processResources') { dependsOn generateRecipeAdvancements }
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public void serverStarted(FMLServerStartedEvent event) {
"56D8B8C1FA7F2289C9F9A3BCF2BEB2D15F0F880373D0647BB9BDBFA7E1D5FE54");
evidence.setProperty(phase.replace('-', '_') + "_complete", "true");
} else if ("fresh".equals(phase)) {
RecipeBookChecks.run(world);
int gameplayChecks = verifyGameplay(server, world);
int worldgenChecks = verifyWorldgen(world);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package zone.moddev.mc.skysgrassslabs.integrationtest;

import com.google.gson.*;
import com.mojang.authlib.GameProfile;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import net.minecraft.advancements.*;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Items;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.*;
import net.minecraft.network.*;
import net.minecraft.server.management.PlayerInteractionManager;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.WorldServer;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.gameevent.PlayerEvent;

/** Checks the real server advancement manager and recipe book, not just the JSON files. */
public final class RecipeBookChecks {
private static final Set<String> MODS = Collections.singleton("skysgrassslabs");

public static void run(WorldServer world) {
SortedMap<String, JsonObject> entries = new TreeMap<>();
List<IRecipe> recipes = new ArrayList<>();
for (IRecipe recipe : CraftingManager.REGISTRY) {
ResourceLocation id = recipe.getRegistryName();
if (id == null || !MODS.contains(id.getNamespace()) || recipe.isDynamic()) continue;
Ingredient first = recipe.getIngredients().stream().filter(i -> i != Ingredient.EMPTY)
.findFirst().orElseThrow(() -> new AssertionError("No book ingredients: " + id));
JsonObject entry = new JsonObject();
entry.addProperty("recipe", id.toString());
entry.addProperty("owner", owner(id));
JsonArray ingredients = new JsonArray();
Set<String> seen = new HashSet<>();
for (ItemStack stack : first.getMatchingStacks()) {
if (stack.isEmpty()) continue;
JsonObject predicate = new JsonObject();
predicate.addProperty("item", stack.getItem().getRegistryName().toString());
if (stack.getMetadata() != 32767) predicate.addProperty("data", stack.getMetadata());
if (seen.add(predicate.toString())) ingredients.add(predicate);
}
require(ingredients.size() > 0, "Empty book ingredients: " + id);
entry.add("ingredients", ingredients);
entries.put(id.toString(), entry);
recipes.add(recipe);
}
File snapshot = new File("recipe-book-snapshot.json");
try (Writer out = new OutputStreamWriter(new FileOutputStream(snapshot), StandardCharsets.UTF_8)) {
new GsonBuilder().setPrettyPrinting().create().toJson(entries.values(), out);
} catch (IOException failure) {
throw new IllegalStateException(failure);
}
if (Boolean.getBoolean("skysbuildingpieces.snapshotRecipeBook")) {
System.out.println("RECIPE_BOOK_SNAPSHOT_ONLY recipes=" + recipes.size());
return;
}
EntityPlayerMP player = new EntityPlayerMP(world.getMinecraftServer(), world,
new GameProfile(UUID.fromString("c64fa100-ff4c-4615-bcbd-90a552320a82"), "RecipeBookProbe"),
new PlayerInteractionManager(world));
List<Packet<?>> sentPackets = new ArrayList<>();
NetworkManager network = new NetworkManager(EnumPacketDirection.SERVERBOUND) {
private final io.netty.channel.Channel testChannel = new io.netty.channel.embedded.EmbeddedChannel();
@Override public io.netty.channel.Channel channel() { return testChannel; }
@Override public void sendPacket(Packet<?> packet) { sentPackets.add(packet); }
};
player.connection = new NetHandlerPlayServer(world.getMinecraftServer(), network, player);
require(!world.getMinecraftServer().getAdvancementManager().hasErrored(), "Advancement parsing failed");
int checks = 0;
List<IRecipe> verified = new ArrayList<>();
for (IRecipe recipe : recipes) {
ResourceLocation id = recipe.getRegistryName();
verified.add(recipe);
Advancement advancement = world.getMinecraftServer().getAdvancementManager()
.getAdvancement(new ResourceLocation(owner(id), "recipes/" + id.getPath()));
require(advancement != null, "Missing recipe advancement: " + id);
require(!recipe.getRecipeOutput().isEmpty(), "Missing book result: " + id);
reset(player, recipe, advancement);
player.inventory.clear();
player.inventory.setInventorySlotContents(0, new ItemStack(Items.APPLE));
CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory);
require(!player.getRecipeBook().isUnlocked(recipe), "Unrelated item unlocked " + id);
Ingredient first = recipe.getIngredients().stream().filter(i -> i != Ingredient.EMPTY).findFirst().get();
ItemStack input = first.getMatchingStacks()[0].copy();
player.inventory.setInventorySlotContents(0, input);
CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory);
require(player.getRecipeBook().isUnlocked(recipe), "Ingredient did not unlock " + id);
if (!id.getNamespace().equals("skysgrassslabs") || !id.getPath().equals("turf")) {
reset(player, recipe, advancement);
player.inventory.clear();
FMLCommonHandler.instance().bus().post(new PlayerEvent.ItemCraftedEvent(player, input, new net.minecraft.inventory.InventoryCrafting(new net.minecraft.inventory.Container() {
@Override public boolean canInteractWith(net.minecraft.entity.player.EntityPlayer ignored) { return true; }
}, 2, 2)));
require(player.getRecipeBook().isUnlocked(recipe), "Crafted ingredient did not unlock " + id);
}
checks++;
}
player.getAdvancements().save();
net.minecraft.nbt.NBTTagCompound savedBook = player.getRecipeBook().write();
net.minecraft.stats.RecipeBookServer reloaded = new net.minecraft.stats.RecipeBookServer();
reloaded.read(savedBook);
for (IRecipe recipe : verified) require(reloaded.isUnlocked(recipe), "Book reload lost " + recipe.getRegistryName());
require(sentPackets.stream().anyMatch(packet -> packet instanceof net.minecraft.network.play.server.SPacketRecipeBook),
"Unlocks did not send recipe-book packets");
player.getAdvancements().dispose();
network.channel().close();
System.out.println("GRASS_RECIPE_BOOK_PASS recipes=" + checks + " inventory=true crafting=true reload=true");
}

private static void reset(EntityPlayerMP player, IRecipe recipe, Advancement advancement) {
player.resetRecipes(Collections.singletonList(recipe));
for (String criterion : advancement.getCriteria().keySet()) {
player.getAdvancements().revokeCriterion(advancement, criterion);
}
}

private static String owner(ResourceLocation recipe) {
return recipe.getNamespace();
}

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

private RecipeBookChecks() { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
public final class SkysGrassSlabs {
public static final String MOD_ID = "skysgrassslabs";
public static final String NAME = "Sky's Grass Slabs";
public static final String VERSION = "1.1.1.112021";
public static final String VERSION = "1.1.2.112021";

@SidedProxy(
clientSide = "zone.moddev.mc.skysgrassslabs.proxy.ClientProxy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public boolean canFit(int width, int height) {
return width * height >= 2;
}

// Legacy material identities are resolved late and cannot be filled safely by the recipe book.
@Override
public boolean isDynamic() {
return true;
}

@Override
public ItemStack getRecipeOutput() {
return new ItemStack(ModBlocks.GRASS_SLAB);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package zone.moddev.mc.skysgrassslabs.recipe;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.Criterion;
import net.minecraft.advancements.ICriterionInstance;
import net.minecraft.advancements.critereon.InventoryChangeTrigger;
import net.minecraft.advancements.critereon.RecipeUnlockedTrigger;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.item.crafting.CraftingManager;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.PlayerEvent;

/** Unlock the next cut while its ingredient is still on the crafting-table cursor. */
@Mod.EventBusSubscriber(modid = "skysgrassslabs")
public final class RecipeBookDiscovery {
private static final Set<String> OWNERS = new HashSet<>(Arrays.asList("skysgrassslabs"));

@SubscribeEvent
public static void crafted(PlayerEvent.ItemCraftedEvent event) {
if (!(event.player instanceof EntityPlayerMP) || event.crafting.isEmpty()) {
return;
}

EntityPlayerMP player = (EntityPlayerMP) event.player;
InventoryPlayer ingredient = new InventoryPlayer(player);
ingredient.setInventorySlotContents(0, event.crafting.copy());

for (IRecipe recipe : CraftingManager.REGISTRY) {
ResourceLocation id = recipe.getRegistryName();
if (id == null || !OWNERS.contains(id.getNamespace()) || recipe.isDynamic()) {
continue;
}
if (recipe.getIngredients().stream().noneMatch(input -> input.apply(event.crafting))) {
continue;
}

for (String owner : OWNERS) {
Advancement advancement = player.getServer().getAdvancementManager()
.getAdvancement(new ResourceLocation(owner, "recipes/" + id.getPath()));
if (advancement == null) {
continue;
}

Criterion recipeCriterion = advancement.getCriteria().get("has_the_recipe");
if (recipeCriterion == null || !(recipeCriterion.getCriterionInstance()
instanceof RecipeUnlockedTrigger.Instance)) {
continue;
}
if (!((RecipeUnlockedTrigger.Instance) recipeCriterion.getCriterionInstance()).test(recipe)) {
continue;
}

// Evaluate the actual criterion, including metadata, rather than guessing its material.
for (Map.Entry<String, Criterion> criterion : advancement.getCriteria().entrySet()) {
ICriterionInstance instance = criterion.getValue().getCriterionInstance();
if (instance instanceof InventoryChangeTrigger.Instance
&& ((InventoryChangeTrigger.Instance) instance).test(ingredient)) {
player.getAdvancements().grantCriterion(advancement, criterion.getKey());
}
}
}
}
}

private RecipeBookDiscovery() {
}
}
Loading
Loading