feat(itsalive): poser le livre culinaire sur un pupitre vanilla #12
+26
-1
@@ -1041,6 +1041,30 @@ tasks.register("verifyItsAlive") {
|
||||
throw new GradleException("Missing culinary substitution-family tag: itsalive:${path}")
|
||||
}
|
||||
}
|
||||
def lecternBooks = new JsonSlurper().parse(file("src/main/resources/data/minecraft/tags/item/lectern_books.json"))
|
||||
def lecternSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryLectern.java").text
|
||||
if (!(lecternBooks.values instanceof List) || !lecternBooks.values.contains("itsalive:culinary_recipe_book")
|
||||
|| !lecternSource.contains("UseBlockCallback.EVENT.register")
|
||||
|| !lecternSource.contains("LecternBlock.HAS_BOOK")
|
||||
|| !lecternSource.contains("CulinaryRecipeKnowledge.inscribe")
|
||||
|| !lecternSource.contains("CulinaryRecipeKnowledge.openBook(serverPlayer, book)")
|
||||
|| !lecternSource.contains("CulinaryRecipeKnowledge.forgetInscription")
|
||||
|| !lecternSource.contains("PlayerBlockBreakEvents.BEFORE")
|
||||
|| !lecternSource.contains("isSecondaryUseActive()")) {
|
||||
throw new GradleException("The culinary recipe book must sit on vanilla lecterns and open its owner's catalogue")
|
||||
}
|
||||
def inscription = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryBookInscription.java").text
|
||||
def components = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java").text
|
||||
def knowledge = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryRecipeKnowledge.java").text
|
||||
def bookItem = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/item/CulinaryRecipeBookItem.java").text
|
||||
if (!inscription.contains("ownerId")
|
||||
|| !components.contains("CULINARY_BOOK")
|
||||
|| !knowledge.contains("openLearned(player, inscription.ownerId())")
|
||||
|| !knowledge.contains("stack.set(ItsAliveDataComponents.CULINARY_BOOK")
|
||||
|| !knowledge.contains("forgetInscription")
|
||||
|| bookItem.contains("openBook(serverPlayer, player.getItemInHand")) {
|
||||
throw new GradleException("A placed culinary book must keep the placer's recipe discoveries only while on a lectern")
|
||||
}
|
||||
def creativeTab = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveCreativeTab.java").text
|
||||
if (!creativeTab.contains('ItsAliveMod.id("agriculture")') || !creativeTab.contains("PLANTING_ITEMS") || !creativeTab.contains("PRODUCE_ITEMS")) {
|
||||
throw new GradleException("The itsalive:agriculture creative tab must expose planting and produce items")
|
||||
@@ -1066,7 +1090,8 @@ tasks.register("verifyItsAlive") {
|
||||
] }
|
||||
def culinaryKeys = (culinaryCatalogItems + ["vinegar", "failed_dish"]).collect { "item.itsalive." + it } + [
|
||||
"tag.item.itsalive.culinary_mushrooms", "tag.item.itsalive.culinary_fish",
|
||||
"tag.item.itsalive.culinary_berries", "tag.item.itsalive.culinary_cooked_meats"
|
||||
"tag.item.itsalive.culinary_berries", "tag.item.itsalive.culinary_cooked_meats",
|
||||
"tooltip.itsalive.culinary_recipe_book.blank"
|
||||
]
|
||||
(livingKeys + moobloomKeys + infectedKeys + culinaryKeys + ["hud.itsalive.hunter.struggle"]).each { key ->
|
||||
if (!language.containsKey(key)) {
|
||||
|
||||
+19
-1
@@ -1,14 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.culinary.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryRecipeKnowledge;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Opens the persistent recipe catalogue without gating recipes the player can guess. */
|
||||
/** Opens the holder's personal catalogue. Shared recipes are only readable on a lectern. */
|
||||
public final class CulinaryRecipeBookItem extends Item {
|
||||
public CulinaryRecipeBookItem(Properties properties) {
|
||||
super(properties);
|
||||
@@ -25,4 +31,16 @@ public final class CulinaryRecipeBookItem extends Item {
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack,
|
||||
TooltipContext context,
|
||||
TooltipDisplay display,
|
||||
Consumer<Component> tooltip,
|
||||
TooltipFlag flag
|
||||
) {
|
||||
tooltip.accept(Component.translatable("tooltip.itsalive.culinary_recipe_book.blank")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.culinary.knowledge;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** Binds a physical culinary book to the player who last placed it on a lectern. */
|
||||
public record CulinaryBookInscription(UUID ownerId, String ownerName) {
|
||||
private static final int MAX_NAME_LENGTH = 32;
|
||||
public static final Codec<CulinaryBookInscription> CODEC = RecordCodecBuilder.create(instance ->
|
||||
instance.group(
|
||||
UUIDUtil.CODEC.fieldOf("owner_id").forGetter(CulinaryBookInscription::ownerId),
|
||||
Codec.STRING.fieldOf("owner_name").forGetter(CulinaryBookInscription::ownerName)
|
||||
).apply(instance, CulinaryBookInscription::new)
|
||||
);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, CulinaryBookInscription> STREAM_CODEC =
|
||||
StreamCodec.composite(
|
||||
UUIDUtil.STREAM_CODEC,
|
||||
CulinaryBookInscription::ownerId,
|
||||
ByteBufCodecs.STRING_UTF8,
|
||||
CulinaryBookInscription::ownerName,
|
||||
CulinaryBookInscription::new
|
||||
);
|
||||
|
||||
public CulinaryBookInscription {
|
||||
if (ownerId == null) {
|
||||
throw new IllegalArgumentException("Culinary book owner is required");
|
||||
}
|
||||
ownerName = sanitizeName(ownerName);
|
||||
}
|
||||
|
||||
public static CulinaryBookInscription of(UUID ownerId, String ownerName) {
|
||||
return new CulinaryBookInscription(ownerId, ownerName);
|
||||
}
|
||||
|
||||
private static String sanitizeName(String ownerName) {
|
||||
String cleaned = ownerName == null ? "" : ownerName.replaceAll("\\p{Cntrl}", "").trim();
|
||||
if (cleaned.length() > MAX_NAME_LENGTH) {
|
||||
cleaned = cleaned.substring(0, MAX_NAME_LENGTH);
|
||||
}
|
||||
return cleaned.isEmpty() ? "?" : cleaned;
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.culinary.knowledge;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing;
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.entity.LecternBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/**
|
||||
* Lets the culinary recipe book sit on a vanilla lectern.
|
||||
* Only a book still on the lectern exposes the placer's catalogue.
|
||||
*/
|
||||
public final class CulinaryLectern {
|
||||
private CulinaryLectern() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
UseBlockCallback.EVENT.register(CulinaryLectern::use);
|
||||
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
|
||||
if (blockEntity instanceof LecternBlockEntity lectern) {
|
||||
CulinaryRecipeKnowledge.forgetInscription(lectern.getBook());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private static InteractionResult use(Player player, Level level, InteractionHand hand, BlockHitResult hit) {
|
||||
if (hand != InteractionHand.MAIN_HAND || player.isSpectator()) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
BlockPos pos = hit.getBlockPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.is(Blocks.LECTERN)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!state.getValue(LecternBlock.HAS_BOOK)) {
|
||||
if (held.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)
|
||||
&& !level.isClientSide()
|
||||
&& player instanceof ServerPlayer serverPlayer) {
|
||||
CulinaryRecipeKnowledge.inscribe(held, serverPlayer);
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (!(level.getBlockEntity(pos) instanceof LecternBlockEntity lectern)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
ItemStack book = lectern.getBook();
|
||||
if (!book.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (level.isClientSide()) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
if (!(player instanceof ServerPlayer serverPlayer) || !level.mayInteract(serverPlayer, pos)) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
if (serverPlayer.isSecondaryUseActive()) {
|
||||
return takeBook(serverPlayer, level, pos, state, lectern);
|
||||
}
|
||||
CulinaryRecipeKnowledge.openBook(serverPlayer, book);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
private static InteractionResult takeBook(
|
||||
ServerPlayer player,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
BlockState state,
|
||||
LecternBlockEntity lectern
|
||||
) {
|
||||
ItemStack taken = lectern.getBook().copy();
|
||||
if (taken.isEmpty()) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
CulinaryRecipeKnowledge.forgetInscription(taken);
|
||||
lectern.setBook(ItemStack.EMPTY);
|
||||
LecternBlock.resetBookState(player, level, pos, state, false);
|
||||
player.getInventory().placeItemBackInInventory(taken);
|
||||
level.playSound(null, pos, SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
+34
-1
@@ -4,12 +4,14 @@ import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryProcessRecipe;
|
||||
import fr.koka99cab.sanctuary26.itsalive.network.CulinaryRecipeBookPayload;
|
||||
import fr.koka99cab.sanctuary26.itsalive.recipe.RedstoneMillingRecipe;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveDataComponents;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -78,6 +80,7 @@ public final class CulinaryRecipeKnowledge {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
CulinaryLectern.initialize();
|
||||
ServerLivingEntityEvents.AFTER_DEATH.register(CulinaryRecipeKnowledge::afterDeath);
|
||||
ItsAliveMod.LOGGER.info(
|
||||
"[{}] Culinary discoveries enabled: books 1/{} hostile, pages 1/{} hostile.",
|
||||
@@ -107,9 +110,39 @@ public final class CulinaryRecipeKnowledge {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void inscribe(ItemStack stack, ServerPlayer player) {
|
||||
if (!stack.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)) {
|
||||
return;
|
||||
}
|
||||
stack.set(ItsAliveDataComponents.CULINARY_BOOK, CulinaryBookInscription.of(
|
||||
player.getUUID(),
|
||||
player.getGameProfile().name()
|
||||
));
|
||||
}
|
||||
|
||||
public static void forgetInscription(ItemStack stack) {
|
||||
if (!stack.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)) {
|
||||
return;
|
||||
}
|
||||
stack.remove(ItsAliveDataComponents.CULINARY_BOOK);
|
||||
}
|
||||
|
||||
public static void openBook(ServerPlayer player) {
|
||||
openLearned(player, player.getUUID());
|
||||
}
|
||||
|
||||
public static void openBook(ServerPlayer player, ItemStack book) {
|
||||
CulinaryBookInscription inscription = book.get(ItsAliveDataComponents.CULINARY_BOOK);
|
||||
if (inscription == null) {
|
||||
openLearned(player, player.getUUID());
|
||||
return;
|
||||
}
|
||||
openLearned(player, inscription.ownerId());
|
||||
}
|
||||
|
||||
private static void openLearned(ServerPlayer player, UUID ownerId) {
|
||||
List<CatalogRecipe> recipes = catalog(player.level().getServer());
|
||||
Set<String> learned = CulinaryRecipeKnowledgeData.get(player.level().getServer()).learned(player.getUUID());
|
||||
Set<String> learned = CulinaryRecipeKnowledgeData.get(player.level().getServer()).learned(ownerId);
|
||||
List<CulinaryRecipeBookPayload.Entry> entries = new ArrayList<>();
|
||||
for (int index = 0; index < recipes.size(); index++) {
|
||||
CatalogRecipe recipe = recipes.get(index);
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryBookInscription;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
|
||||
/** Persistent item components owned by It's Alive. */
|
||||
public final class ItsAliveDataComponents {
|
||||
public static final DataComponentType<CulinaryBookInscription> CULINARY_BOOK = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
ItsAliveMod.id("culinary_book"),
|
||||
DataComponentType.<CulinaryBookInscription>builder()
|
||||
.persistent(CulinaryBookInscription.CODEC)
|
||||
.networkSynchronized(CulinaryBookInscription.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
|
||||
private ItsAliveDataComponents() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
}
|
||||
}
|
||||
+1
@@ -21,6 +21,7 @@ public final class ItsAliveRegistries {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
ItsAliveDataComponents.initialize();
|
||||
ItsAliveEntityTypes.register();
|
||||
ItsAliveBlocks.register();
|
||||
ItsAliveItems.register();
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"item.itsalive.burger": "Burger",
|
||||
"item.itsalive.culinary_recipe_book": "Culinary Recipe Book",
|
||||
"item.itsalive.recipe_page": "Recipe Page",
|
||||
"tooltip.itsalive.culinary_recipe_book.blank": "Place it on a lectern to share your recipes.",
|
||||
"message.itsalive.recipe_page.learned": "Recipe discovered: %s (page %s/%s)",
|
||||
"message.itsalive.recipe_page.complete": "You already know every culinary recipe.",
|
||||
"book.itsalive.appliance.mill": "Redstone mill",
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"item.itsalive.burger": "Burger",
|
||||
"item.itsalive.culinary_recipe_book": "Livre de recettes culinaires",
|
||||
"item.itsalive.recipe_page": "Page de recette",
|
||||
"tooltip.itsalive.culinary_recipe_book.blank": "Pose-le sur un pupitre pour partager tes recettes.",
|
||||
"message.itsalive.recipe_page.learned": "Recette découverte : %s (page %s/%s)",
|
||||
"message.itsalive.recipe_page.complete": "Tu connais déjà toutes les recettes culinaires.",
|
||||
"book.itsalive.appliance.mill": "Moulin redstone",
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"item.itsalive.burger": "Burger",
|
||||
"item.itsalive.culinary_recipe_book": "Culinary Recipe Book",
|
||||
"item.itsalive.recipe_page": "Recipe Page",
|
||||
"tooltip.itsalive.culinary_recipe_book.blank": "Положи на кафедру, чтобы поделиться рецептами.",
|
||||
"message.itsalive.recipe_page.learned": "Recipe discovered: %s (page %s/%s)",
|
||||
"message.itsalive.recipe_page.complete": "You already know every culinary recipe.",
|
||||
"book.itsalive.appliance.mill": "Redstone mill",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"itsalive:culinary_recipe_book"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user