Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02901c9cbc | ||
|
|
d6fd32994f | ||
|
|
143da0a940 | ||
|
|
dc5157f973 |
No files matched your search
@@ -6,6 +6,8 @@ It is not an official release of Mystcraft.
|
|||||||
Mystcraft Legacy source: https://github.com/Mystcraft/Mystcraft-Legacy
|
Mystcraft Legacy source: https://github.com/Mystcraft/Mystcraft-Legacy
|
||||||
Mystcraft Legacy copyright: XCompWiz and contributors
|
Mystcraft Legacy copyright: XCompWiz and contributors
|
||||||
Mystcraft Legacy license: GNU Lesser General Public License version 3
|
Mystcraft Legacy license: GNU Lesser General Public License version 3
|
||||||
|
The animated ink-vial item texture in this project is copied from Mystcraft
|
||||||
|
Legacy's assets/mystcraft/textures/items/ink_vial.png under that license.
|
||||||
|
|
||||||
The initial build layout is derived from the official NeoForge 1.21.1
|
The initial build layout is derived from the official NeoForge 1.21.1
|
||||||
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
|
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
|
||||||
@@ -13,8 +13,13 @@ have not been ported yet.
|
|||||||
|
|
||||||
Milestone 4 now includes stackable blank Pages plus server-created symbol,
|
Milestone 4 now includes stackable blank Pages plus server-created symbol,
|
||||||
unresolved-symbol, and baseline link-panel states. Page visuals follow the
|
unresolved-symbol, and baseline link-panel states. Page visuals follow the
|
||||||
legacy runtime-composition rules; written-page creation will become available
|
legacy runtime-composition rules. Craft an Ink Vial from two black dye, a glass
|
||||||
through the Writing Desk work that follows.
|
bottle, and a water bucket; the bucket is returned. Hold a blank Page in one
|
||||||
|
hand and use the Ink Vial in the other to prepare a baseline Link Panel Page.
|
||||||
|
Each vial holds 16 ink units, and the default link-panel cost is two units.
|
||||||
|
The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
|
||||||
|
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
|
||||||
|
Symbol-page transcription will become available through the Writing Desk.
|
||||||
|
|
||||||
## Linking Book
|
## Linking Book
|
||||||
|
|
||||||
|
|||||||
@@ -64,3 +64,20 @@ identifier. Built-in pages reproduce Mystcraft Legacy's four-word poem
|
|||||||
composition from its page background and symbol-component sheet. Data-pack
|
composition from its page background and symbol-component sheet. Data-pack
|
||||||
symbols without a bundled resource-pack model use the legacy question-mark
|
symbols without a bundled resource-pack model use the legacy question-mark
|
||||||
fallback while keeping their real identifier and display metadata.
|
fallback while keeping their real identifier and display metadata.
|
||||||
|
|
||||||
|
## Ink and baseline link panels
|
||||||
|
|
||||||
|
An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket.
|
||||||
|
The bucket remains after crafting. Each vial holds 16 ink units, stored in a
|
||||||
|
persistent, network-synchronized component; only vials with identical remaining
|
||||||
|
ink stack together. When depleted, a vial returns a glass bottle that can be
|
||||||
|
used to craft another. The animated item texture comes from Mystcraft Legacy.
|
||||||
|
|
||||||
|
The server config sets ink costs independently for symbol copying (default one
|
||||||
|
unit) and baseline link-panel preparation (default two units). To prepare a
|
||||||
|
panel, hold a blank Page in one hand and use an Ink Vial in the other. The
|
||||||
|
server checks the held items, blank state, and sufficient ink before consuming
|
||||||
|
anything. A stack of blank Pages yields exactly one Link Panel Page per use;
|
||||||
|
if the inventory is full, the result is dropped instead of deleted. Symbol
|
||||||
|
copying will be implemented by the Writing Desk. Link-property modifiers are
|
||||||
|
deferred beyond this baseline flow.
|
||||||
+1
-1
@@ -35,7 +35,7 @@ mod_name=Rivencraft
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.3.3.2
|
mod_version=0.3.4.1
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
# This should match the base package used for the mod sources.
|
# This should match the base package used for the mod sources.
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||||
import net.dnorberg.rivencraft.network.RenameNotebookPagePayload;
|
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.Button;
|
import net.minecraft.client.gui.components.Button;
|
||||||
import net.minecraft.client.gui.components.EditBox;
|
import net.minecraft.client.gui.components.EditBox;
|
||||||
import net.minecraft.client.gui.components.Tooltip;
|
|
||||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.inventory.ClickType;
|
import net.minecraft.world.inventory.ClickType;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.neoforged.neoforge.network.PacketDistributor;
|
|
||||||
|
|
||||||
/** A page surface modelled on Legacy's folder GUI, backed by the Notebook menu. */
|
/** A page surface modelled on Legacy's folder GUI, backed by the Notebook menu. */
|
||||||
public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu> {
|
public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu> {
|
||||||
@@ -28,7 +25,6 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
private static final int PAGE_STEP_Y = 41;
|
private static final int PAGE_STEP_Y = 41;
|
||||||
private static final int PAGE_COLUMNS = 5;
|
private static final int PAGE_COLUMNS = 5;
|
||||||
private EditBox textInput;
|
private EditBox textInput;
|
||||||
private boolean renaming;
|
|
||||||
private boolean draggingScrollbar;
|
private boolean draggingScrollbar;
|
||||||
private boolean surfaceClickPendingRelease;
|
private boolean surfaceClickPendingRelease;
|
||||||
private int selectedSlot = -1;
|
private int selectedSlot = -1;
|
||||||
@@ -47,23 +43,17 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
Button sortButton = Button.builder(Component.literal("AZ"), button -> {})
|
Button sortButton = Button.builder(Component.literal("AZ"), button -> {})
|
||||||
.bounds(leftPos, topPos, 18, 18).build();
|
.bounds(leftPos, topPos, 18, 18).build();
|
||||||
Button allButton = Button.builder(Component.literal("ALL"), button -> {})
|
Button allButton = Button.builder(Component.literal("ALL"), button -> {})
|
||||||
.bounds(leftPos + 20, topPos, 18, 18).build();
|
.bounds(leftPos + 20, topPos, 26, 18).build();
|
||||||
sortButton.active = false;
|
sortButton.active = false;
|
||||||
allButton.active = false;
|
allButton.active = false;
|
||||||
addRenderableWidget(sortButton);
|
addRenderableWidget(sortButton);
|
||||||
addRenderableWidget(allButton);
|
addRenderableWidget(allButton);
|
||||||
textInput = new EditBox(font, leftPos + 40, topPos, 94, 18,
|
textInput = new EditBox(font, leftPos + 48, topPos, 128, 18,
|
||||||
Component.translatable("screen.rivencraft.notebook.search"));
|
Component.translatable("screen.rivencraft.notebook.search"));
|
||||||
textInput.setMaxLength(32);
|
textInput.setMaxLength(32);
|
||||||
textInput.setHint(Component.translatable("screen.rivencraft.notebook.search"));
|
textInput.setHint(Component.translatable("screen.rivencraft.notebook.search"));
|
||||||
textInput.setResponder(value -> {
|
textInput.setResponder(value -> scroll = 0);
|
||||||
if (!renaming) scroll = 0;
|
|
||||||
});
|
|
||||||
addRenderableWidget(textInput);
|
addRenderableWidget(textInput);
|
||||||
addRenderableWidget(Button.builder(Component.literal("R"), button -> toggleRename())
|
|
||||||
.bounds(leftPos + 136, topPos, 18, 18)
|
|
||||||
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.notebook.rename_toggle")))
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -120,7 +110,6 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
if (index >= 0 && (button == 0 || button == 1)) {
|
if (index >= 0 && (button == 0 || button == 1)) {
|
||||||
ItemStack page = menu.getSlot(index).getItem();
|
ItemStack page = menu.getSlot(index).getItem();
|
||||||
if (!page.isEmpty() && !matchesSearch(page)) return true;
|
if (!page.isEmpty() && !matchesSearch(page)) return true;
|
||||||
if (renaming) finishRename();
|
|
||||||
setFocused(null);
|
setFocused(null);
|
||||||
textInput.setFocused(false);
|
textInput.setFocused(false);
|
||||||
selectedSlot = index;
|
selectedSlot = index;
|
||||||
@@ -163,12 +152,15 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
if (renaming && textInput.isFocused() && (keyCode == 257 || keyCode == 335)) {
|
if (textInput.isFocused()) {
|
||||||
finishRename();
|
if (keyCode == 256) {
|
||||||
return true;
|
setFocused(null);
|
||||||
}
|
textInput.setFocused(false);
|
||||||
if (!textInput.isFocused() && keyCode == 82 && selectedSlot >= 0) {
|
} else {
|
||||||
toggleRename();
|
textInput.keyPressed(keyCode, scanCode, modifiers);
|
||||||
|
}
|
||||||
|
// EditBox does not consume ordinary letter key presses; the matching charTyped
|
||||||
|
// event handles text input. Do not pass them to the container's inventory keys.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!textInput.isFocused() && selectedSlot >= 0 && (keyCode == 263 || keyCode == 262)) {
|
if (!textInput.isFocused() && selectedSlot >= 0 && (keyCode == 263 || keyCode == 262)) {
|
||||||
@@ -178,12 +170,6 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClose() {
|
|
||||||
commitRename();
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> visiblePages() {
|
private List<Integer> visiblePages() {
|
||||||
List<Integer> pages = new ArrayList<>();
|
List<Integer> pages = new ArrayList<>();
|
||||||
int firstEmpty = -1;
|
int firstEmpty = -1;
|
||||||
@@ -202,7 +188,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean matchesSearch(ItemStack page) {
|
private boolean matchesSearch(ItemStack page) {
|
||||||
return renaming || textInput.getValue().isBlank()
|
return textInput.getValue().isBlank()
|
||||||
|| page.getHoverName().getString().toLowerCase(Locale.ROOT)
|
|| page.getHoverName().getString().toLowerCase(Locale.ROOT)
|
||||||
.contains(textInput.getValue().toLowerCase(Locale.ROOT));
|
.contains(textInput.getValue().toLowerCase(Locale.ROOT));
|
||||||
}
|
}
|
||||||
@@ -273,32 +259,6 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleRename() {
|
|
||||||
if (renaming) {
|
|
||||||
finishRename();
|
|
||||||
} else if (selectedSlot >= 0 && !menu.getSlot(selectedSlot).getItem().isEmpty()) {
|
|
||||||
renaming = true;
|
|
||||||
textInput.setValue(menu.getSlot(selectedSlot).getItem().getHoverName().getString());
|
|
||||||
textInput.setHint(Component.translatable("screen.rivencraft.notebook.rename"));
|
|
||||||
setFocused(textInput);
|
|
||||||
textInput.setFocused(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finishRename() {
|
|
||||||
commitRename();
|
|
||||||
renaming = false;
|
|
||||||
textInput.setValue("");
|
|
||||||
textInput.setHint(Component.translatable("screen.rivencraft.notebook.search"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void commitRename() {
|
|
||||||
if (renaming && selectedSlot >= 0 && !menu.getSlot(selectedSlot).getItem().isEmpty()) {
|
|
||||||
PacketDistributor.sendToServer(new RenameNotebookPagePayload(
|
|
||||||
menu.containerId, selectedSlot, textInput.getValue()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void moveSelected(boolean next) {
|
private void moveSelected(boolean next) {
|
||||||
int target = selectedSlot + (next ? 1 : -1);
|
int target = selectedSlot + (next ? 1 : -1);
|
||||||
if (selectedSlot < 0 || target < 0 || target >= NotebookMenu.NOTEBOOK_SLOTS
|
if (selectedSlot < 0 || target < 0 || target >= NotebookMenu.NOTEBOOK_SLOTS
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public final class RivencraftConfig {
|
|||||||
|
|
||||||
public static final ModConfigSpec SPEC;
|
public static final ModConfigSpec SPEC;
|
||||||
private static final ModConfigSpec.EnumValue<ReturnLinkMode> RETURN_LINK_MODE;
|
private static final ModConfigSpec.EnumValue<ReturnLinkMode> RETURN_LINK_MODE;
|
||||||
|
private static final ModConfigSpec.IntValue SYMBOL_INK_COST;
|
||||||
|
private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||||
@@ -25,6 +27,12 @@ public final class RivencraftConfig {
|
|||||||
)
|
)
|
||||||
.defineEnum("return_link_mode", ReturnLinkMode.ON_CREATION);
|
.defineEnum("return_link_mode", ReturnLinkMode.ON_CREATION);
|
||||||
builder.pop();
|
builder.pop();
|
||||||
|
builder.push("writing");
|
||||||
|
SYMBOL_INK_COST = builder.comment("Ink units consumed when the Writing Desk copies one symbol page.")
|
||||||
|
.defineInRange("symbol_ink_cost", 1, 1, 16);
|
||||||
|
LINK_PANEL_INK_COST = builder.comment("Ink units consumed when preparing one baseline link-panel page.")
|
||||||
|
.defineInRange("link_panel_ink_cost", 2, 1, 16);
|
||||||
|
builder.pop();
|
||||||
SPEC = builder.build();
|
SPEC = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,4 +42,12 @@ public final class RivencraftConfig {
|
|||||||
public static ReturnLinkMode returnLinkMode() {
|
public static ReturnLinkMode returnLinkMode() {
|
||||||
return RETURN_LINK_MODE.get();
|
return RETURN_LINK_MODE.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int symbolInkCost() {
|
||||||
|
return SYMBOL_INK_COST.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int linkPanelInkCost() {
|
||||||
|
return LINK_PANEL_INK_COST.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ import net.dnorberg.rivencraft.writing.SymbolReference;
|
|||||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||||
@@ -22,6 +24,7 @@ import net.minecraft.resources.ResourceLocation;
|
|||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.Items;
|
import net.minecraft.world.item.Items;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.inventory.ClickType;
|
||||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
@@ -150,32 +153,145 @@ public final class WritingDataGameTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void notebookMenuValidatesInsertionAndPersistsReorderAndRename(GameTestHelper helper) {
|
public static void notebookMenuValidatesInsertionAndPersistsReorder(GameTestHelper helper) {
|
||||||
var player = helper.makeMockServerPlayerInLevel();
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get());
|
ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get());
|
||||||
player.setItemInHand(InteractionHand.MAIN_HAND, notebook);
|
player.setItemInHand(InteractionHand.MAIN_HAND, notebook);
|
||||||
NotebookMenu menu = new NotebookMenu(7, player.getInventory(), InteractionHand.MAIN_HAND);
|
NotebookMenu menu = new NotebookMenu(7, player.getInventory(), InteractionHand.MAIN_HAND);
|
||||||
ItemStack first = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
|
ItemStack first = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
|
||||||
|
first.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,
|
||||||
|
net.minecraft.network.chat.Component.literal("Terrain Base"));
|
||||||
ItemStack second = PageItem.createRecoveredPage(reference("example:removed_symbol"));
|
ItemStack second = PageItem.createRecoveredPage(reference("example:removed_symbol"));
|
||||||
|
|
||||||
helper.assertFalse(menu.getSlot(0).mayPlace(new ItemStack(Items.DIRT)),
|
helper.assertFalse(menu.getSlot(0).mayPlace(new ItemStack(Items.DIRT)),
|
||||||
"Notebook slots must reject non-Page items");
|
"Notebook slots must reject non-Page items");
|
||||||
menu.getSlot(0).set(first);
|
menu.getSlot(0).set(first);
|
||||||
menu.getSlot(1).set(second);
|
menu.getSlot(1).set(second);
|
||||||
menu.renamePage(0, "Terrain Base");
|
|
||||||
helper.assertTrue(menu.clickMenuButton(player, NotebookMenu.MOVE_NEXT_BASE),
|
helper.assertTrue(menu.clickMenuButton(player, NotebookMenu.MOVE_NEXT_BASE),
|
||||||
"The server menu must accept a valid adjacent reorder");
|
"The server menu must accept a valid adjacent reorder");
|
||||||
|
|
||||||
NotebookContents saved = notebook.get(ModDataComponents.NOTEBOOK_CONTENTS.get());
|
NotebookContents saved = notebook.get(ModDataComponents.NOTEBOOK_CONTENTS.get());
|
||||||
helper.assertTrue(saved != null, "Menu changes must persist immediately to the Notebook item");
|
helper.assertTrue(saved != null, "Menu changes must persist immediately to the Notebook item");
|
||||||
helper.assertValueEqual(saved.pages().get(1).getHoverName().getString(), "Terrain Base",
|
helper.assertValueEqual(saved.pages().get(1).getHoverName().getString(), "Terrain Base",
|
||||||
"Reordering must preserve the renamed Page stack");
|
"Reordering must preserve existing Page item data");
|
||||||
helper.assertValueEqual(PageItem.content(saved.pages().get(0)).kind(),
|
helper.assertValueEqual(PageItem.content(saved.pages().get(0)).kind(),
|
||||||
PageContent.Kind.UNRESOLVED_SYMBOL,
|
PageContent.Kind.UNRESOLVED_SYMBOL,
|
||||||
"Reordering must preserve unresolved pages");
|
"Reordering must preserve unresolved pages");
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void notebookMenuSurvivesInventoryStackReplacementAndClicks(GameTestHelper helper) {
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.NOTEBOOK.get()));
|
||||||
|
NotebookMenu menu = new NotebookMenu(8, player.getInventory(), InteractionHand.MAIN_HAND);
|
||||||
|
|
||||||
|
// Slot synchronization may replace the ItemStack object while the Notebook remains held.
|
||||||
|
player.setItemInHand(InteractionHand.MAIN_HAND, player.getMainHandItem().copy());
|
||||||
|
helper.assertTrue(menu.stillValid(player),
|
||||||
|
"Replacing the held stack object must not close an open Notebook menu");
|
||||||
|
menu.getSlot(0).set(PageItem.createSymbolPage(reference("rivencraft:terrain/standard")));
|
||||||
|
helper.assertTrue(player.getMainHandItem().has(ModDataComponents.NOTEBOOK_CONTENTS.get()),
|
||||||
|
"Page edits must persist to the current held Notebook stack");
|
||||||
|
|
||||||
|
player.getInventory().setItem(9, new ItemStack(Items.DIRT));
|
||||||
|
menu.clicked(NotebookMenu.NOTEBOOK_SLOTS, 0, ClickType.PICKUP, player);
|
||||||
|
helper.assertTrue(menu.getCarried().is(Items.DIRT),
|
||||||
|
"Clicking a player inventory item must pick it up without closing the Notebook");
|
||||||
|
helper.assertTrue(menu.stillValid(player),
|
||||||
|
"Normal inventory clicks must leave the Notebook menu valid");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) {
|
||||||
|
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
|
||||||
|
vial.set(ModDataComponents.INK_REMAINING.get(), 7);
|
||||||
|
CompoundTag serialized = (CompoundTag) vial.saveOptional(helper.getLevel().registryAccess());
|
||||||
|
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), serialized);
|
||||||
|
helper.assertValueEqual(InkVialItem.remaining(restored), 7,
|
||||||
|
"Remaining ink must survive ItemStack persistence");
|
||||||
|
helper.assertTrue(ItemStack.isSameItemSameComponents(vial, restored),
|
||||||
|
"Vials with equal ink contents must be eligible to stack");
|
||||||
|
helper.assertFalse(ItemStack.isSameItemSameComponents(vial, new ItemStack(ModItems.INK_VIAL.get())),
|
||||||
|
"Vials with different ink contents must not stack");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void linkPanelPreparationConsumesOnePageAndConfiguredInkOnlyOnSuccess(GameTestHelper helper) {
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
ItemStack pages = new ItemStack(ModItems.PAGE.get(), 2);
|
||||||
|
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
|
||||||
|
player.setItemInHand(InteractionHand.MAIN_HAND, vial);
|
||||||
|
player.setItemInHand(InteractionHand.OFF_HAND, pages);
|
||||||
|
|
||||||
|
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||||
|
"A blank Page and sufficient ink must prepare one link panel");
|
||||||
|
helper.assertValueEqual(pages.getCount(), 1, "Preparation must consume exactly one blank Page");
|
||||||
|
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
|
||||||
|
InkVialItem.CAPACITY - WritingInkService.linkPanelCost(),
|
||||||
|
"Preparation must consume exactly the server-configured ink cost");
|
||||||
|
long panels = player.getInventory().items.stream()
|
||||||
|
.filter(stack -> stack.is(ModItems.PAGE.get())
|
||||||
|
&& PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL)
|
||||||
|
.mapToInt(ItemStack::getCount).sum();
|
||||||
|
helper.assertValueEqual(panels, 1L, "Preparation must output exactly one link panel");
|
||||||
|
|
||||||
|
ItemStack written = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
|
||||||
|
player.setItemInHand(InteractionHand.OFF_HAND, written);
|
||||||
|
int inkBefore = InkVialItem.remaining(player.getMainHandItem());
|
||||||
|
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||||
|
"A written Page must not be overwritten as a link panel");
|
||||||
|
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), inkBefore,
|
||||||
|
"Rejected writing must not consume ink");
|
||||||
|
helper.assertValueEqual(PageItem.content(written).kind(), PageContent.Kind.SYMBOL,
|
||||||
|
"Rejected writing must not modify the input Page");
|
||||||
|
|
||||||
|
ItemStack lowInk = new ItemStack(ModItems.INK_VIAL.get());
|
||||||
|
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost() - 1);
|
||||||
|
player.setItemInHand(InteractionHand.MAIN_HAND, lowInk);
|
||||||
|
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get()));
|
||||||
|
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||||
|
"Insufficient ink must reject link-panel preparation");
|
||||||
|
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
|
||||||
|
WritingInkService.linkPanelCost() - 1,
|
||||||
|
"Insufficient ink must remain untouched");
|
||||||
|
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), PageContent.Kind.BLANK,
|
||||||
|
"Insufficient ink must leave the Page blank");
|
||||||
|
|
||||||
|
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost());
|
||||||
|
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||||
|
"Exact remaining ink must be usable");
|
||||||
|
helper.assertTrue(player.getMainHandItem().is(Items.GLASS_BOTTLE),
|
||||||
|
"A depleted Ink Vial must return a reusable glass bottle");
|
||||||
|
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(),
|
||||||
|
PageContent.Kind.LINK_PANEL,
|
||||||
|
"A single blank Page must be replaced by one Link Panel Page");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void stackedInkVialsSplitWithoutChangingTheOtherVials(GameTestHelper helper) {
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.INK_VIAL.get(), 2));
|
||||||
|
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get()));
|
||||||
|
|
||||||
|
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||||
|
"A stack of full vials must support preparing one panel");
|
||||||
|
helper.assertValueEqual(player.getMainHandItem().getCount(), 1,
|
||||||
|
"Using one vial must leave the other full vial in hand");
|
||||||
|
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), InkVialItem.CAPACITY,
|
||||||
|
"The unconsumed vial must remain full");
|
||||||
|
long partialVials = player.getInventory().items.stream()
|
||||||
|
.filter(stack -> stack.is(ModItems.INK_VIAL.get())
|
||||||
|
&& InkVialItem.remaining(stack) == InkVialItem.CAPACITY - WritingInkService.linkPanelCost())
|
||||||
|
.mapToInt(ItemStack::getCount).sum();
|
||||||
|
helper.assertValueEqual(partialVials, 1L,
|
||||||
|
"The used vial must reappear with only its own ink reduced");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
private static SymbolReference reference(String id) {
|
private static SymbolReference reference(String id) {
|
||||||
return new SymbolReference(ResourceLocation.parse(id));
|
return new SymbolReference(ResourceLocation.parse(id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package net.dnorberg.rivencraft.item;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.InteractionResultHolder;
|
||||||
|
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.level.Level;
|
||||||
|
|
||||||
|
/** A stackable vial; the ink component is per vial, not shared across a stack. */
|
||||||
|
public final class InkVialItem extends Item {
|
||||||
|
public static final int CAPACITY = 16;
|
||||||
|
|
||||||
|
public InkVialItem(Properties properties) {
|
||||||
|
super(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int remaining(ItemStack vial) {
|
||||||
|
return vial.getOrDefault(ModDataComponents.INK_REMAINING.get(), CAPACITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||||
|
if (!WritingInkService.canPrepareLinkPanel(player, hand)) {
|
||||||
|
return InteractionResultHolder.pass(player.getItemInHand(hand));
|
||||||
|
}
|
||||||
|
if (!level.isClientSide) {
|
||||||
|
WritingInkService.prepareLinkPanel(player, hand);
|
||||||
|
}
|
||||||
|
return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendHoverText(ItemStack stack, TooltipContext context,
|
||||||
|
List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
tooltip.add(Component.translatable("item.rivencraft.ink_vial.remaining",
|
||||||
|
remaining(stack), CAPACITY).withStyle(ChatFormatting.GRAY));
|
||||||
|
tooltip.add(Component.translatable("item.rivencraft.ink_vial.usage")
|
||||||
|
.withStyle(ChatFormatting.GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,7 @@ import net.dnorberg.rivencraft.registry.ModItems;
|
|||||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
import net.minecraft.core.NonNullList;
|
import net.minecraft.core.NonNullList;
|
||||||
import net.minecraft.core.component.DataComponents;
|
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.network.chat.Component;
|
|
||||||
import net.minecraft.world.Container;
|
import net.minecraft.world.Container;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.SimpleContainer;
|
import net.minecraft.world.SimpleContainer;
|
||||||
@@ -26,7 +24,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
|||||||
private static final int PLAYER_INVENTORY_START = NOTEBOOK_SLOTS;
|
private static final int PLAYER_INVENTORY_START = NOTEBOOK_SLOTS;
|
||||||
private final Inventory playerInventory;
|
private final Inventory playerInventory;
|
||||||
private final InteractionHand hand;
|
private final InteractionHand hand;
|
||||||
private final ItemStack notebook;
|
private final int openedHotbarSlot;
|
||||||
private final NotebookContainer contents;
|
private final NotebookContainer contents;
|
||||||
|
|
||||||
public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) {
|
public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||||
@@ -37,8 +35,8 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
|||||||
super(ModMenus.NOTEBOOK.get(), containerId);
|
super(ModMenus.NOTEBOOK.get(), containerId);
|
||||||
this.playerInventory = inventory;
|
this.playerInventory = inventory;
|
||||||
this.hand = hand;
|
this.hand = hand;
|
||||||
this.notebook = inventory.player.getItemInHand(hand);
|
this.openedHotbarSlot = inventory.selected;
|
||||||
this.contents = new NotebookContainer(notebook);
|
this.contents = new NotebookContainer(inventory, hand);
|
||||||
|
|
||||||
for (int row = 0; row < 3; row++) {
|
for (int row = 0; row < 3; row++) {
|
||||||
for (int column = 0; column < 9; column++) {
|
for (int column = 0; column < 9; column++) {
|
||||||
@@ -58,14 +56,16 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Slot playerSlot(int inventoryIndex, int x, int y) {
|
private Slot playerSlot(int inventoryIndex, int x, int y) {
|
||||||
boolean locked = hand == InteractionHand.MAIN_HAND && inventoryIndex == playerInventory.selected;
|
boolean locked = hand == InteractionHand.MAIN_HAND && inventoryIndex == openedHotbarSlot;
|
||||||
return locked ? new LockedSlot(playerInventory, inventoryIndex, x, y)
|
return locked ? new LockedSlot(playerInventory, inventoryIndex, x, y)
|
||||||
: new Slot(playerInventory, inventoryIndex, x, y);
|
: new Slot(playerInventory, inventoryIndex, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean stillValid(Player player) {
|
public boolean stillValid(Player player) {
|
||||||
return player.isAlive() && player.getItemInHand(hand) == notebook && notebook.is(ModItems.NOTEBOOK.get());
|
return player.isAlive()
|
||||||
|
&& (hand == InteractionHand.OFF_HAND || player.getInventory().selected == openedHotbarSlot)
|
||||||
|
&& player.getItemInHand(hand).is(ModItems.NOTEBOOK.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -116,29 +116,18 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void renamePage(int slot, String name) {
|
|
||||||
if (slot < 0 || slot >= NOTEBOOK_SLOTS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ItemStack page = contents.getItem(slot);
|
|
||||||
if (page.isEmpty() || !page.is(ModItems.PAGE.get())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (name.isBlank()) {
|
|
||||||
page.remove(DataComponents.CUSTOM_NAME);
|
|
||||||
} else {
|
|
||||||
page.set(DataComponents.CUSTOM_NAME, Component.literal(name));
|
|
||||||
}
|
|
||||||
contents.setChanged();
|
|
||||||
broadcastChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clicked(int slotId, int button, ClickType clickType, Player player) {
|
public void clicked(int slotId, int button, ClickType clickType, Player player) {
|
||||||
super.clicked(slotId, button, clickType, player);
|
|
||||||
if (!stillValid(player)) {
|
if (!stillValid(player)) {
|
||||||
player.closeContainer();
|
player.closeContainer();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (clickType == ClickType.SWAP
|
||||||
|
&& (button == openedHotbarSlot && hand == InteractionHand.MAIN_HAND
|
||||||
|
|| button == 40 && hand == InteractionHand.OFF_HAND)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.clicked(slotId, button, clickType, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class PageSlot extends Slot {
|
private static final class PageSlot extends Slot {
|
||||||
@@ -160,12 +149,15 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class NotebookContainer extends SimpleContainer {
|
private static final class NotebookContainer extends SimpleContainer {
|
||||||
private final ItemStack notebook;
|
private final Inventory inventory;
|
||||||
|
private final InteractionHand hand;
|
||||||
private boolean loading;
|
private boolean loading;
|
||||||
|
|
||||||
private NotebookContainer(ItemStack notebook) {
|
private NotebookContainer(Inventory inventory, InteractionHand hand) {
|
||||||
super(NOTEBOOK_SLOTS);
|
super(NOTEBOOK_SLOTS);
|
||||||
this.notebook = notebook;
|
this.inventory = inventory;
|
||||||
|
this.hand = hand;
|
||||||
|
ItemStack notebook = inventory.player.getItemInHand(hand);
|
||||||
loading = true;
|
loading = true;
|
||||||
List<ItemStack> saved = notebook.getOrDefault(
|
List<ItemStack> saved = notebook.getOrDefault(
|
||||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||||
@@ -187,7 +179,8 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
|||||||
@Override
|
@Override
|
||||||
public void setChanged() {
|
public void setChanged() {
|
||||||
super.setChanged();
|
super.setChanged();
|
||||||
if (loading || notebook.isEmpty()) {
|
ItemStack notebook = inventory.player.getItemInHand(hand);
|
||||||
|
if (loading || !notebook.is(ModItems.NOTEBOOK.get())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NonNullList<ItemStack> saved = NonNullList.withSize(NOTEBOOK_SLOTS, ItemStack.EMPTY);
|
NonNullList<ItemStack> saved = NonNullList.withSize(NOTEBOOK_SLOTS, ItemStack.EMPTY);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
|
|||||||
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
|
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
|
||||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
@@ -50,8 +49,6 @@ public final class ModNetworking {
|
|||||||
|
|
||||||
public static void register(RegisterPayloadHandlersEvent event) {
|
public static void register(RegisterPayloadHandlersEvent event) {
|
||||||
PayloadRegistrar registrar = event.registrar("10");
|
PayloadRegistrar registrar = event.registrar("10");
|
||||||
registrar.playToServer(RenameNotebookPagePayload.TYPE, RenameNotebookPagePayload.STREAM_CODEC,
|
|
||||||
ModNetworking::renameNotebookPage);
|
|
||||||
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
|
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
|
||||||
ModNetworking::requestDescriptiveBookScreen);
|
ModNetworking::requestDescriptiveBookScreen);
|
||||||
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
|
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
|
||||||
@@ -73,16 +70,6 @@ public final class ModNetworking {
|
|||||||
ClientAgeCelestialAngles::handle);
|
ClientAgeCelestialAngles::handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void renameNotebookPage(RenameNotebookPagePayload payload, IPayloadContext context) {
|
|
||||||
if (!(context.player() instanceof ServerPlayer player)
|
|
||||||
|| !(player.containerMenu instanceof NotebookMenu menu)
|
|
||||||
|| menu.containerId != payload.containerId()
|
|
||||||
|| payload.name().length() > MAX_BOOK_TITLE_LENGTH) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
menu.renamePage(payload.slot(), normalizeTitle(payload.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
|
private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
|
||||||
if (!(context.player() instanceof ServerPlayer player)) {
|
if (!(context.player() instanceof ServerPlayer player)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package net.dnorberg.rivencraft.network;
|
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
|
||||||
import net.minecraft.network.codec.ByteBufCodecs;
|
|
||||||
import net.minecraft.network.codec.StreamCodec;
|
|
||||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
|
||||||
|
|
||||||
public record RenameNotebookPagePayload(int containerId, int slot, String name) implements CustomPacketPayload {
|
|
||||||
public static final Type<RenameNotebookPagePayload> TYPE = new Type<>(
|
|
||||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "rename_notebook_page"));
|
|
||||||
public static final StreamCodec<ByteBuf, RenameNotebookPagePayload> STREAM_CODEC = StreamCodec.composite(
|
|
||||||
ByteBufCodecs.VAR_INT, RenameNotebookPagePayload::containerId,
|
|
||||||
ByteBufCodecs.VAR_INT, RenameNotebookPagePayload::slot,
|
|
||||||
ByteBufCodecs.STRING_UTF8, RenameNotebookPagePayload::name,
|
|
||||||
RenameNotebookPagePayload::new);
|
|
||||||
|
|
||||||
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.registry;
|
package net.dnorberg.rivencraft.registry;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.world.item.CreativeModeTab;
|
import net.minecraft.world.item.CreativeModeTab;
|
||||||
@@ -19,8 +20,18 @@ public final class ModCreativeTabs {
|
|||||||
.displayItems((parameters, output) -> {
|
.displayItems((parameters, output) -> {
|
||||||
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
|
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
|
||||||
output.accept(ModItems.LINKING_BOOK.get());
|
output.accept(ModItems.LINKING_BOOK.get());
|
||||||
output.accept(ModItems.PAGE.get());
|
|
||||||
output.accept(ModItems.NOTEBOOK.get());
|
output.accept(ModItems.NOTEBOOK.get());
|
||||||
|
output.accept(ModItems.INK_VIAL.get());
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
|
||||||
|
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> PAGES =
|
||||||
|
CREATIVE_TABS.register("rivencraft_pages", () -> CreativeModeTab.builder()
|
||||||
|
.title(Component.translatable("itemGroup.rivencraft.pages"))
|
||||||
|
.icon(PageItem::createLinkPanelPage)
|
||||||
|
.displayItems((parameters, output) -> {
|
||||||
|
output.accept(ModItems.PAGE.get());
|
||||||
|
output.accept(PageItem.createLinkPanelPage());
|
||||||
})
|
})
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.book.AgeReference;
|
|||||||
import net.dnorberg.rivencraft.writing.PageContent;
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
import net.minecraft.core.component.DataComponentType;
|
import net.minecraft.core.component.DataComponentType;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -60,6 +61,12 @@ public final class ModDataComponents {
|
|||||||
.networkSynchronized(NotebookContents.STREAM_CODEC)
|
.networkSynchronized(NotebookContents.STREAM_CODEC)
|
||||||
.cacheEncoding()
|
.cacheEncoding()
|
||||||
.build());
|
.build());
|
||||||
|
public static final DeferredHolder<DataComponentType<?>, DataComponentType<Integer>> INK_REMAINING =
|
||||||
|
DATA_COMPONENTS.register("ink_remaining", () -> DataComponentType.<Integer>builder()
|
||||||
|
.persistent(Codec.intRange(0, 16))
|
||||||
|
.networkSynchronized(ByteBufCodecs.VAR_INT)
|
||||||
|
.cacheEncoding()
|
||||||
|
.build());
|
||||||
|
|
||||||
private ModDataComponents() {
|
private ModDataComponents() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
|||||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.dnorberg.rivencraft.item.NotebookItem;
|
import net.dnorberg.rivencraft.item.NotebookItem;
|
||||||
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
import net.minecraft.world.item.Item;
|
import net.minecraft.world.item.Item;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
import net.neoforged.neoforge.registries.DeferredItem;
|
import net.neoforged.neoforge.registries.DeferredItem;
|
||||||
@@ -33,6 +34,11 @@ public final class ModItems {
|
|||||||
NotebookItem::new,
|
NotebookItem::new,
|
||||||
new Item.Properties().stacksTo(1)
|
new Item.Properties().stacksTo(1)
|
||||||
);
|
);
|
||||||
|
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
||||||
|
"ink_vial",
|
||||||
|
InkVialItem::new,
|
||||||
|
new Item.Properties().stacksTo(16)
|
||||||
|
);
|
||||||
|
|
||||||
private ModItems() {
|
private ModItems() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||||
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
|
||||||
|
/** Server-owned costs and the baseline blank-page-to-link-panel transaction. */
|
||||||
|
public final class WritingInkService {
|
||||||
|
private WritingInkService() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int symbolCopyCost() {
|
||||||
|
return RivencraftConfig.symbolInkCost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int linkPanelCost() {
|
||||||
|
return RivencraftConfig.linkPanelInkCost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canPrepareLinkPanel(Player player, InteractionHand vialHand) {
|
||||||
|
ItemStack vial = player.getItemInHand(vialHand);
|
||||||
|
ItemStack page = player.getItemInHand(other(vialHand));
|
||||||
|
int remaining = InkVialItem.remaining(vial);
|
||||||
|
return vial.is(ModItems.INK_VIAL.get()) && remaining >= linkPanelCost()
|
||||||
|
&& remaining <= InkVialItem.CAPACITY
|
||||||
|
&& page.is(ModItems.PAGE.get()) && PageItem.content(page).kind() == PageContent.Kind.BLANK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean prepareLinkPanel(Player player, InteractionHand vialHand) {
|
||||||
|
if (player.level().isClientSide || !canPrepareLinkPanel(player, vialHand)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractionHand pageHand = other(vialHand);
|
||||||
|
ItemStack page = player.getItemInHand(pageHand);
|
||||||
|
ItemStack vial = player.getItemInHand(vialHand);
|
||||||
|
int newInk = InkVialItem.remaining(vial) - linkPanelCost();
|
||||||
|
ItemStack panel = PageItem.createLinkPanelPage();
|
||||||
|
ItemStack spentVial = newInk == 0
|
||||||
|
? new ItemStack(Items.GLASS_BOTTLE)
|
||||||
|
: new ItemStack(ModItems.INK_VIAL.get());
|
||||||
|
if (newInk > 0) {
|
||||||
|
spentVial.set(ModDataComponents.INK_REMAINING.get(), newInk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All validation and output construction is complete before changing either input.
|
||||||
|
if (page.getCount() == 1) {
|
||||||
|
player.setItemInHand(pageHand, panel);
|
||||||
|
} else {
|
||||||
|
page.shrink(1);
|
||||||
|
giveOrDrop(player, panel);
|
||||||
|
}
|
||||||
|
if (vial.getCount() == 1) {
|
||||||
|
player.setItemInHand(vialHand, spentVial);
|
||||||
|
} else {
|
||||||
|
vial.shrink(1);
|
||||||
|
giveOrDrop(player, spentVial);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InteractionHand other(InteractionHand hand) {
|
||||||
|
return hand == InteractionHand.MAIN_HAND ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void giveOrDrop(Player player, ItemStack stack) {
|
||||||
|
if (!player.getInventory().add(stack)) {
|
||||||
|
player.drop(stack, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
"item.rivencraft.notebook": "Notebook",
|
"item.rivencraft.notebook": "Notebook",
|
||||||
"item.rivencraft.notebook.pages": "Pages: %s/%s",
|
"item.rivencraft.notebook.pages": "Pages: %s/%s",
|
||||||
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
|
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
|
||||||
"screen.rivencraft.notebook.rename": "Rename selected page",
|
"item.rivencraft.ink_vial": "Ink Vial",
|
||||||
|
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
||||||
|
"item.rivencraft.ink_vial.usage": "Use with a blank Page in the other hand to prepare a Link Panel.",
|
||||||
"screen.rivencraft.notebook.search": "Search pages",
|
"screen.rivencraft.notebook.search": "Search pages",
|
||||||
"screen.rivencraft.notebook.rename_toggle": "Rename selected page (R); use left/right arrows to reorder",
|
|
||||||
"screen.rivencraft.notebook.move_previous": "Move selected page left",
|
"screen.rivencraft.notebook.move_previous": "Move selected page left",
|
||||||
"screen.rivencraft.notebook.move_next": "Move selected page right",
|
"screen.rivencraft.notebook.move_next": "Move selected page right",
|
||||||
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
|
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
|
||||||
@@ -97,5 +98,6 @@
|
|||||||
"screen.rivencraft.book.explore": "Explore",
|
"screen.rivencraft.book.explore": "Explore",
|
||||||
"screen.rivencraft.book.exploring": "Exploring",
|
"screen.rivencraft.book.exploring": "Exploring",
|
||||||
"screen.rivencraft.book.return_from_explore": "Right-click to return",
|
"screen.rivencraft.book.return_from_explore": "Right-click to return",
|
||||||
"itemGroup.rivencraft": "Rivencraft"
|
"itemGroup.rivencraft": "Rivencraft",
|
||||||
|
"itemGroup.rivencraft.pages": "Rivencraft Pages"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"parent": "minecraft:item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "rivencraft:item/ink_vial"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"animation": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:crafting_shapeless",
|
||||||
|
"category": "misc",
|
||||||
|
"ingredients": [
|
||||||
|
{ "item": "minecraft:black_dye" },
|
||||||
|
{ "item": "minecraft:black_dye" },
|
||||||
|
{ "item": "minecraft:glass_bottle" },
|
||||||
|
{ "item": "minecraft:water_bucket" }
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"count": 1,
|
||||||
|
"id": "rivencraft:ink_vial"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user