Public Access
This commit is contained in:
1 parent
be8440ddb5
commit
35980338e1
43 files changed
+1850
-90
No files matched your search
@@ -95,6 +95,8 @@ public final class Rivencraft {
|
||||
COMPONENTS.registerComponentType("notebook_contents", builder -> builder
|
||||
.persistent(net.dnorberg.rivencraft.writing.NotebookContents.CODEC)
|
||||
.networkSynchronized(net.dnorberg.rivencraft.writing.NotebookContents.STREAM_CODEC));
|
||||
public static final Supplier<DataComponentType<String>> LINK_DESTINATION_NAME = COMPONENTS.registerComponentType(
|
||||
"link_destination_name", builder -> builder.persistent(Codec.STRING).networkSynchronized(ByteBufCodecs.STRING_UTF8));
|
||||
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
|
||||
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
||||
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
||||
@@ -106,6 +108,8 @@ public final class Rivencraft {
|
||||
"symbol_page_id", builder -> builder.persistent(Identifier.CODEC)
|
||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||
/** Ordered authored page IDs remain on the finished book for later port slices. */
|
||||
public static final Supplier<DataComponentType<String>> BOOK_AUTHOR = COMPONENTS.registerComponentType(
|
||||
"book_author", builder -> builder.persistent(Codec.STRING).networkSynchronized(ByteBufCodecs.STRING_UTF8));
|
||||
public static final Supplier<DataComponentType<java.util.List<Identifier>>> BOOK_SYMBOLS = COMPONENTS.registerComponentType(
|
||||
"book_symbols", builder -> builder.persistent(Identifier.CODEC.listOf())
|
||||
.networkSynchronized(ByteBufCodecs.<io.netty.buffer.ByteBuf, Identifier>list(252)
|
||||
@@ -133,6 +137,12 @@ public final class Rivencraft {
|
||||
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
|
||||
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
|
||||
StarFissureBlockEntity::new, STAR_FISSURE.get()));
|
||||
public static final DeferredBlock<net.dnorberg.rivencraft.block.BookStandBlock> BOOK_STAND =
|
||||
BLOCKS.registerBlock("book_stand", net.dnorberg.rivencraft.block.BookStandBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.LECTERN));
|
||||
public static final Supplier<BlockEntityType<net.dnorberg.rivencraft.block.entity.BookStandBlockEntity>> BOOK_STAND_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("book_stand", () -> new BlockEntityType<>(
|
||||
net.dnorberg.rivencraft.block.entity.BookStandBlockEntity::new, BOOK_STAND.get()));
|
||||
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||
"book_binder", PortBookBinderBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
@@ -219,6 +229,7 @@ public final class Rivencraft {
|
||||
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre);
|
||||
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAcceleratedTicks::onLevelTickPost);
|
||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.event.BookStandConversionHandler::onRightClickBlock);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onServerStopped);
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package net.dnorberg.rivencraft.advancement;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Awards action-only progression after the server has committed the action. */
|
||||
public final class ProgressionAdvancements {
|
||||
public static final String LINK_PANEL = "link_panel";
|
||||
public static final String COPY_PAGE = "copy_page";
|
||||
public static final String BIND_AGE = "bind_age";
|
||||
public static final String ENTER_AGE = "enter_age";
|
||||
|
||||
private ProgressionAdvancements() {}
|
||||
|
||||
public static void award(ServerPlayer player, String path) {
|
||||
var advancement = player.level().getServer().getAdvancements().get(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "progression/" + path));
|
||||
if (advancement != null) player.getAdvancements().award(advancement, "completed");
|
||||
}
|
||||
|
||||
public static void awardAgeTravel(ServerPlayer player) {
|
||||
if (RuntimeAgeBackend.savedAges(player.level().getServer()).containsKey(player.level().dimension().identifier()))
|
||||
award(player, ENTER_AGE);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,9 @@ final class AgeLevelIndex extends SavedData {
|
||||
Codec.unboundedMap(Identifier.CODEC, PortAgeColors.CODEC)
|
||||
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortDayCycle.CODEC)
|
||||
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles)
|
||||
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles),
|
||||
Codec.unboundedMap(Identifier.CODEC, Codec.STRING)
|
||||
.optionalFieldOf("display_names", Map.of()).forGetter(index -> index.displayNames)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -65,6 +67,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
private final Map<Identifier, PortSkyStyle> sky;
|
||||
private final Map<Identifier, PortAgeColors> colors;
|
||||
private final Map<Identifier, PortDayCycle> dayCycles;
|
||||
private final Map<Identifier, String> displayNames;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
@@ -81,6 +84,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.sky = new LinkedHashMap<>();
|
||||
this.colors = new LinkedHashMap<>();
|
||||
this.dayCycles = new LinkedHashMap<>();
|
||||
this.displayNames = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
@@ -94,7 +98,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) {
|
||||
Map<Identifier, PortDayCycle> dayCycles, Map<Identifier, String> displayNames) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
@@ -109,6 +113,16 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.sky = new LinkedHashMap<>(sky);
|
||||
this.colors = new LinkedHashMap<>(colors);
|
||||
this.dayCycles = new LinkedHashMap<>(dayCycles);
|
||||
this.displayNames = new LinkedHashMap<>(displayNames);
|
||||
}
|
||||
|
||||
String displayName(Identifier id) { return ages.containsKey(id) ? displayNames.get(id) : null; }
|
||||
|
||||
void setDisplayName(Identifier id, String name) {
|
||||
if (!ages.containsKey(id)) throw new IllegalArgumentException("Age not indexed: " + id);
|
||||
if (name == null || name.isBlank() || name.length() > 32) throw new IllegalArgumentException("Invalid Age display name");
|
||||
displayNames.put(id, name.strip());
|
||||
setDirty();
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
|
||||
@@ -35,6 +35,12 @@ public final class PortAgeCommands {
|
||||
public static void register(RegisterCommandsEvent event) {
|
||||
event.getDispatcher().register(Commands.literal("rivencraft_port")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
||||
.then(Commands.literal("recipe_discovery_probe")
|
||||
.executes(context -> {
|
||||
PortProgressionProbe.recipeDiscovery(context.getSource().getServer().overworld());
|
||||
context.getSource().sendSuccess(() -> Component.literal("Recipe discovery checks passed for all six crafting unlocks"), false);
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("compat")
|
||||
.executes(context -> {
|
||||
StringBuilder mods = new StringBuilder("Optional mods: ");
|
||||
@@ -69,6 +75,44 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("create_drop_conversion_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortBookDropConversionProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Drop sources queued; run drop_conversion_probe after server tasks complete"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("drop_conversion_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortBookDropConversionProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Universal book drop conversion/ownership checks passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_book_stand_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortBookStandProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Book Stand conversion/ownership/source checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("book_stand_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortBookStandProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Book Stand persistence passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_writing_desk_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
@@ -79,6 +123,15 @@ public final class PortAgeCommands {
|
||||
context.getSource().sendSuccess(() -> Component.literal("Writing Desk transaction/removal checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("progression_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortProgressionProbe.run(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Progression data/action checks passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("writing_desk_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
|
||||
+59
-1
@@ -52,7 +52,7 @@ final class PortBinderCollectionProbe {
|
||||
if (!binder.importCollection(folder, -1).isEmpty() || !binder.importCollection(folder, 1).isEmpty())
|
||||
throw new IllegalStateException("Invalid collection insertion point accepted");
|
||||
binder.insertPage(0, page("terrain/flat"));
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
var player = new PortProgressionPlayer(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("5225a1ed-5ead-44b5-a63c-c78e7dca86f5"), "RivenBinderProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
var menu = new PortBookBinderMenu(1, player.getInventory(), binder);
|
||||
@@ -81,6 +81,52 @@ final class PortBinderCollectionProbe {
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.COVER).isEmpty()
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty())
|
||||
throw new IllegalStateException("Collection-backed Binder assembly regression");
|
||||
if (!PortProgressionProbe.isComplete(player, "bind_age"))
|
||||
throw new IllegalStateException("Missing bind_age award");
|
||||
if (!player.getGameProfile().name().equals(book.get(Rivencraft.BOOK_AUTHOR.get())))
|
||||
throw new IllegalStateException("Binder author was not server assigned");
|
||||
var pages = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(book);
|
||||
if (pages.lastPage() != EXPECTED.size() || pages.symbolAt(0).isPresent()
|
||||
|| !pages.symbolAt(1).orElseThrow().equals(EXPECTED.getFirst())
|
||||
|| !pages.symbolAt(EXPECTED.size()).orElseThrow().equals(EXPECTED.getLast())
|
||||
|| pages.symbolAt(EXPECTED.size() + 1).isPresent()
|
||||
|| pages.turn(0, -1) != 0 || pages.turn(pages.lastPage(), 1) != pages.lastPage())
|
||||
throw new IllegalStateException("Authored book page indexing failed");
|
||||
var old = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()));
|
||||
if (old.lastPage() != 1 || !old.author().isEmpty() || old.symbolAt(1).isPresent())
|
||||
throw new IllegalStateException("Older panel-only book fallback failed");
|
||||
var repeated = new net.dnorberg.rivencraft.writing.DescriptiveBookPages(
|
||||
List.of(EXPECTED.getFirst(), EXPECTED.getFirst()), "Test");
|
||||
if (repeated.lastPage() != 2 || !repeated.symbolAt(1).equals(repeated.symbolAt(2)))
|
||||
throw new IllegalStateException("Repeated symbol display was collapsed");
|
||||
var ageId = book.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
var server = level.getServer();
|
||||
if (!RuntimeAgeBackend.savedAgeName(server, ageId).orElseThrow().equals("Collection Fixture104"))
|
||||
throw new IllegalStateException("Created Age lost its display name");
|
||||
var namedLevel = server.getLevel(net.minecraft.resources.ResourceKey.create(
|
||||
net.minecraft.core.registries.Registries.DIMENSION, ageId));
|
||||
if (!net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(namedLevel).equals("Collection Fixture104"))
|
||||
throw new IllegalStateException("Link binding name lookup did not use Age title");
|
||||
RuntimeAgeBackend.setAgeDisplayName(server, ageId, "Crystal Garden's Dawn");
|
||||
var target = new net.dnorberg.rivencraft.item.LinkBookTarget(ageId, -0.5, 64, -1.25, 90, 0);
|
||||
var link = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
link.set(Rivencraft.LINK_BOOK_TARGET.get(), target);
|
||||
link.set(Rivencraft.LINK_DESTINATION_NAME.get(), net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(namedLevel));
|
||||
if (!net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(link, target).getString().equals("Crystal Garden's Dawn"))
|
||||
throw new IllegalStateException("Link destination label ignored saved name");
|
||||
var mode = net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.get();
|
||||
player.getInventory().clearContent();
|
||||
try {
|
||||
net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.set(net.dnorberg.rivencraft.PortConfig.ReturnLinkMode.ALWAYS);
|
||||
PortReturnLinks.afterSuccessfulTravel(player, ageId, target);
|
||||
var returned = java.util.stream.IntStream.range(0, player.getInventory().getContainerSize())
|
||||
.mapToObj(i -> player.getInventory().getItem(i)).filter(i -> i.is(Rivencraft.LINKING_BOOK.get()))
|
||||
.findFirst().orElseThrow();
|
||||
if (!"Crystal Garden's Dawn".equals(returned.get(Rivencraft.LINK_DESTINATION_NAME.get())))
|
||||
throw new IllegalStateException("Automatic Return Link lost departure name");
|
||||
chest.setItem(4, returned.copy());
|
||||
} finally { net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.set(mode); }
|
||||
chest.setItem(3, link);
|
||||
chest.setItem(2, book.copy());
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
} finally { player.getInventory().clearContent(); }
|
||||
@@ -93,6 +139,18 @@ final class PortBinderCollectionProbe {
|
||||
throw new IllegalStateException("Empty collection covers were not preserved");
|
||||
for (int i = 0; i < 2; i++) if (chest.getItem(i).get(Rivencraft.NOTEBOOK_CONTENTS.get()).pageCount() != 0)
|
||||
throw new IllegalStateException("Imported pages remained in a collection");
|
||||
if (!"RivenBinderProbe".equals(chest.getItem(2).get(Rivencraft.BOOK_AUTHOR.get())))
|
||||
throw new IllegalStateException("Authored book lost its author");
|
||||
var ageId = chest.getItem(2).get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (!RuntimeAgeBackend.savedAgeName(level.getServer(), ageId).orElseThrow().equals("Crystal Garden's Dawn")
|
||||
|| !"Crystal Garden's Dawn".equals(chest.getItem(3).get(Rivencraft.LINK_DESTINATION_NAME.get()))
|
||||
|| !"Crystal Garden's Dawn".equals(chest.getItem(4).get(Rivencraft.LINK_DESTINATION_NAME.get())))
|
||||
throw new IllegalStateException("Age/Linking/Return destination names did not survive persistence");
|
||||
var old = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
var overworld = new net.dnorberg.rivencraft.item.LinkBookTarget(net.minecraft.world.level.Level.OVERWORLD.identifier(), 0, 64, 0, 0, 0);
|
||||
if (!net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(old, overworld).getString()
|
||||
.equals(net.minecraft.network.chat.Component.translatable("dimension.minecraft.overworld").getString()))
|
||||
throw new IllegalStateException("Older book vanilla name fallback changed");
|
||||
if (!EXPECTED.equals(chest.getItem(2).get(Rivencraft.BOOK_SYMBOLS.get())))
|
||||
throw new IllegalStateException("Authored book page order was not preserved");
|
||||
}
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.TickTask;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.TagValueOutput;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Join conversion runs on queued server tasks: create and inspect on separate commands. */
|
||||
final class PortBookDropConversionProbe {
|
||||
private static final UUID OWNER = UUID.fromString("cc5909e5-aafa-43d6-b841-a917ec9c0115");
|
||||
private static final AABB AREA = new AABB(-16, -64, -16, 32, 320, 32);
|
||||
private static ItemStack book(boolean descriptive, String label) {
|
||||
var stack = new ItemStack(descriptive ? Rivencraft.DESCRIPTIVE_BOOK.get() : Rivencraft.LINKING_BOOK.get());
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(label));
|
||||
return stack;
|
||||
}
|
||||
static void create(ServerLevel level) {
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) level.setBlock(new BlockPos(x, 63, z), Blocks.STONE.defaultBlockState(), 2);
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(UUID.fromString("84f38604-48fe-48d3-a4fb-f858a7200115"), "RivenDropProbe"));
|
||||
player.setPos(8.5, 64, 8.5);
|
||||
// Test exact factory state before physics can advance position or pickup timers.
|
||||
var sample = new ItemEntity(level, 8.5, 66, 8.5, book(false, "state_copy"));
|
||||
sample.getItem().set(Rivencraft.LINK_BOOK_TARGET.get(), new net.dnorberg.rivencraft.item.LinkBookTarget(
|
||||
level.dimension().identifier(), -0.5, 64, -1.25, 135, -20));
|
||||
sample.getItem().set(Rivencraft.LINK_DESTINATION_NAME.get(), "Named destination");
|
||||
sample.getItem().set(Rivencraft.LINK_PROPERTIES.get(), new net.dnorberg.rivencraft.item.LinkProperties(net.dnorberg.rivencraft.item.LinkProperties.DISARM
|
||||
| net.dnorberg.rivencraft.item.LinkProperties.MAINTAIN_MOMENTUM));
|
||||
sample.setTarget(OWNER);
|
||||
sample.setThrower(player);
|
||||
sample.setPickUpDelay(80);
|
||||
sample.setDeltaMovement(new Vec3(0.2, 0.1, -0.3));
|
||||
sample.setYRot(37);
|
||||
sample.setXRot(12);
|
||||
sample.setNoGravity(true);
|
||||
var copy = DroppedBookEntity.fromDrop(sample);
|
||||
if (!copy.position().equals(sample.position()) || !copy.getDeltaMovement().equals(sample.getDeltaMovement())
|
||||
|| copy.getYRot() != 37 || copy.getXRot() != 12 || !copy.isNoGravity()
|
||||
|| !ItemStack.matches(copy.getItem(), sample.getItem()) || !OWNER.equals(copy.getTarget())
|
||||
|| copy.getUUID().equals(sample.getUUID()) || copy.getAge() != -32768)
|
||||
throw new IllegalStateException("Drop conversion lost placement, ownership or item components");
|
||||
var originalTag = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess());
|
||||
var copiedTag = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess());
|
||||
sample.saveWithoutId(originalTag); copy.saveWithoutId(copiedTag);
|
||||
for (String key : List.of("PickupDelay", "Owner", "Thrower", "Item"))
|
||||
if (!java.util.Objects.equals(originalTag.buildResult().get(key), copiedTag.buildResult().get(key)))
|
||||
throw new IllegalStateException("Drop conversion lost metadata: " + key);
|
||||
// Four ordinary book variants must all convert through the actual event route.
|
||||
for (String name : List.of("manual_link", "manual_description", "unwritten_link", "unwritten_description")) {
|
||||
var stack = book(name.endsWith("description"), name);
|
||||
if (name.equals("manual_link")) {
|
||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), sample.getItem().get(Rivencraft.LINK_BOOK_TARGET.get()));
|
||||
stack.set(Rivencraft.LINK_DESTINATION_NAME.get(), "Named destination");
|
||||
}
|
||||
if (name.equals("manual_description")) stack.set(Rivencraft.AGE_BOOK_ID.get(), level.dimension().identifier());
|
||||
var item = new ItemEntity(level, 8.5, 66, 8.5, stack);
|
||||
item.setNoGravity(true); item.setDeltaMovement(Vec3.ZERO); item.setTarget(OWNER);
|
||||
level.addFreshEntity(item);
|
||||
}
|
||||
player.drop(book(false, "player_toss"), false, net.minecraft.util.Prediction.SERVER_ONLY);
|
||||
net.minecraft.world.Containers.dropItemStack(level, 8.5, 65, 8.5, book(true, "container"));
|
||||
var standPos = new BlockPos(10, 64, 8);
|
||||
level.setBlock(standPos, Rivencraft.BOOK_STAND.get().defaultBlockState(), 3);
|
||||
((net.dnorberg.rivencraft.block.entity.BookStandBlockEntity) level.getBlockEntity(standPos)).setBook(book(false, "stand"));
|
||||
level.destroyBlock(standPos, true);
|
||||
var custom = new DroppedBookEntity(level, new Vec3(8.5, 66, 8.5), book(false, "already_custom"));
|
||||
custom.setNoGravity(true);
|
||||
level.addFreshEntity(custom);
|
||||
level.addFreshEntity(new ItemEntity(level, 8.5, 66, 8.5, net.dnorberg.rivencraft.item.PageItem.createLinkPanelPage()));
|
||||
java.util.function.Consumer<net.neoforged.neoforge.event.entity.EntityJoinLevelEvent> cancel = event -> {
|
||||
if (event.getLevel() == level && event.getEntity() instanceof DroppedBookEntity dropped
|
||||
&& dropped.getItem().getHoverName().getString().equals("cancel_preserve")) event.setCanceled(true);
|
||||
};
|
||||
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(cancel);
|
||||
try {
|
||||
var item = new ItemEntity(level, 8.5, 66, 8.5, book(false, "cancel_preserve"));
|
||||
item.setNoGravity(true); item.setDeltaMovement(Vec3.ZERO);
|
||||
level.addFreshEntity(item);
|
||||
} finally {
|
||||
// Queued after that source's conversion task; no fixture listener remains installed.
|
||||
level.getServer().schedule(new TickTask(0, () -> net.neoforged.neoforge.common.NeoForge.EVENT_BUS.unregister(cancel)));
|
||||
}
|
||||
}
|
||||
static void verify(ServerLevel level) {
|
||||
var drops = level.getEntitiesOfClass(ItemEntity.class, AREA);
|
||||
for (String name : List.of("manual_link", "manual_description", "unwritten_link", "unwritten_description",
|
||||
"player_toss", "container", "stand", "already_custom", "cancel_preserve")) {
|
||||
var found = drops.stream().filter(e -> e.getItem().getHoverName().getString().equals(name)).toList();
|
||||
if (found.size() != 1 || !name.equals("cancel_preserve") && !(found.getFirst() instanceof DroppedBookEntity))
|
||||
throw new IllegalStateException("Expected one open book for " + name + ", found " + found.size());
|
||||
}
|
||||
var named = drops.stream().filter(e -> e.getItem().getHoverName().getString().equals("manual_link")).findFirst().orElseThrow();
|
||||
if (!OWNER.equals(named.getTarget()) || !"Named destination".equals(named.getItem().get(Rivencraft.LINK_DESTINATION_NAME.get())))
|
||||
throw new IllegalStateException("Converted book lost owner or destination");
|
||||
if (drops.stream().filter(e -> e.getItem().is(Rivencraft.PAGE.get())).anyMatch(e -> e instanceof DroppedBookEntity))
|
||||
throw new IllegalStateException("Non-book item was converted");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.UseBookStandPayload;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Isolated conversion, ownership, stale-screen and persistence checks. */
|
||||
final class PortBookStandProbe {
|
||||
private static final BlockPos POS = new BlockPos(0, 64, 0);
|
||||
static void create(ServerLevel level) {
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("a5c323a1-3b58-4dd6-927f-e08000800110"), "RivenStandProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
try {
|
||||
for (Direction facing : Direction.Plane.HORIZONTAL) for (InteractionHand hand : InteractionHand.values())
|
||||
for (var sample : java.util.List.of(new ItemStack(Rivencraft.LINKING_BOOK.get()),
|
||||
new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()), PageItem.createLinkPanelPage().copyWithCount(3))) {
|
||||
level.setBlock(POS, Blocks.LECTERN.defaultBlockState().setValue(LecternBlock.FACING, facing), 3);
|
||||
player.setItemInHand(hand, sample.copy());
|
||||
if (!BookStandConversionHandler.convert(player, hand, POS)) throw new IllegalStateException("Conversion failed");
|
||||
var stand = (BookStandBlockEntity) level.getBlockEntity(POS);
|
||||
var stored = sample.copyWithCount(1);
|
||||
if (!ItemStack.matches(stand.getBook(), stored) || player.getItemInHand(hand).getCount() != sample.getCount() - 1
|
||||
|| stand.getBlockState().getValue(BookStandBlock.FACING) != facing)
|
||||
throw new IllegalStateException("Conversion lost ownership or facing");
|
||||
var packet = new UseBookStandPayload(level.dimension().identifier(), POS, stored.copy());
|
||||
if (PortBookNetworking.validatedStandBook(player, packet).isEmpty()) throw new IllegalStateException("Valid source rejected");
|
||||
player.setPos(100, 64, 100);
|
||||
if (!PortBookNetworking.validatedStandBook(player, packet).isEmpty()) throw new IllegalStateException("Distant source accepted");
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
if (!PortBookNetworking.validatedStandBook(player, new UseBookStandPayload(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft", "overworld"), POS, stored)).isEmpty())
|
||||
throw new IllegalStateException("Wrong dimension accepted");
|
||||
if (!PortBookNetworking.validatedStandBook(player, new UseBookStandPayload(packet.dimension(), POS, new ItemStack(Items.DIRT))).isEmpty())
|
||||
throw new IllegalStateException("Replaced book accepted");
|
||||
var loaded = net.minecraft.world.level.block.entity.BlockEntity.loadStatic(POS, stand.getBlockState(),
|
||||
stand.saveWithFullMetadata(level.registryAccess()), level.registryAccess());
|
||||
if (!(loaded instanceof BookStandBlockEntity copy) || !ItemStack.matches(copy.getBook(), stored))
|
||||
throw new IllegalStateException("Stand serialization lost content");
|
||||
player.getInventory().clearContent();
|
||||
player.setShiftKeyDown(true);
|
||||
stand.getBlockState().useWithoutItem(level, player, new BlockHitResult(Vec3.atCenterOf(POS), Direction.UP, POS, false));
|
||||
player.setShiftKeyDown(false);
|
||||
if (!level.getBlockState(POS).is(Blocks.LECTERN)
|
||||
|| level.getBlockState(POS).getValue(LecternBlock.FACING) != facing
|
||||
|| player.getInventory().countItem(stored.getItem()) != 1
|
||||
|| !PortBookNetworking.validatedStandBook(player, packet).isEmpty())
|
||||
throw new IllegalStateException("Retrieval did not restore lectern or recover item exactly once");
|
||||
player.getInventory().clearContent();
|
||||
player.setItemInHand(hand, stored.copy());
|
||||
BookStandConversionHandler.convert(player, hand, POS);
|
||||
level.destroyBlock(POS, true);
|
||||
var drops = level.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,
|
||||
new net.minecraft.world.phys.AABB(-2, 62, -2, 3, 68, 3));
|
||||
int items = drops.stream().filter(e -> e.getItem().is(stored.getItem())).mapToInt(e -> e.getItem().getCount()).sum();
|
||||
int lecterns = drops.stream().filter(e -> e.getItem().is(Items.LECTERN)).mapToInt(e -> e.getItem().getCount()).sum();
|
||||
if (items != 1 || lecterns != 1) throw new IllegalStateException("Break drops duplicated/lost: " + items + "/" + lecterns);
|
||||
drops.forEach(net.minecraft.world.entity.Entity::discard);
|
||||
}
|
||||
level.setBlock(POS, Blocks.LECTERN.defaultBlockState().setValue(LecternBlock.HAS_BOOK, true), 3);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, PageItem.createLinkPanelPage());
|
||||
if (BookStandConversionHandler.convert(player, InteractionHand.MAIN_HAND, POS)) throw new IllegalStateException("Occupied lectern converted");
|
||||
level.setBlock(POS, Blocks.LECTERN.defaultBlockState(), 3);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.DIRT));
|
||||
if (BookStandConversionHandler.convert(player, InteractionHand.MAIN_HAND, POS)) throw new IllegalStateException("Unrelated item accepted");
|
||||
var named = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
named.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Stand persistence"));
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, named);
|
||||
if (!BookStandConversionHandler.convert(player, InteractionHand.MAIN_HAND, POS)) throw new IllegalStateException("Final stand missing");
|
||||
} finally { player.setShiftKeyDown(false); player.getInventory().clearContent(); }
|
||||
verify(level);
|
||||
}
|
||||
static void verify(ServerLevel level) {
|
||||
if (!(level.getBlockEntity(POS) instanceof BookStandBlockEntity stand)
|
||||
|| !stand.getBook().is(Rivencraft.LINKING_BOOK.get())
|
||||
|| !stand.getBook().getHoverName().getString().equals("Stand persistence"))
|
||||
throw new IllegalStateException("Stand restart persistence failed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.server.PlayerAdvancements;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixers;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.neoforge.common.util.FakePlayer;
|
||||
|
||||
/** FakePlayer normally discards all advancement progress; fixtures need vanilla tracking. */
|
||||
final class PortProgressionPlayer extends FakePlayer {
|
||||
private PlayerAdvancements progress;
|
||||
|
||||
PortProgressionPlayer(ServerLevel level, GameProfile profile) { super(level, profile); }
|
||||
|
||||
@Override public PlayerAdvancements getAdvancements() {
|
||||
if (progress == null) {
|
||||
var server = level().getServer();
|
||||
progress = new PlayerAdvancements(DataFixers.getDataFixer(), server.getPlayerList(),
|
||||
server.getAdvancements(), server.getWorldPath(LevelResource.PLAYER_ADVANCEMENTS_DIR)
|
||||
.resolve(getUUID() + ".json"), this);
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.advancement.ProgressionAdvancements;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Disposable operator fixture for loaded advancement data and committed action awards. */
|
||||
final class PortProgressionProbe {
|
||||
/** Operator-fixture assertion; deliberately requires the real loaded advancement. */
|
||||
static boolean isComplete(net.minecraft.server.level.ServerPlayer player, String path) {
|
||||
var advancement = player.level().getServer().getAdvancements().get(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "progression/" + path));
|
||||
if (advancement == null) throw new IllegalStateException("Missing progression advancement: " + path);
|
||||
return player.getAdvancements().getOrStartProgress(advancement).isDone();
|
||||
}
|
||||
|
||||
|
||||
static void recipeDiscovery(ServerLevel level) {
|
||||
var materials = java.util.Map.of(
|
||||
"ink_vial", Items.DYE.black(),
|
||||
"ink_mixer", Items.GLASS_BOTTLE,
|
||||
"book_binder", Items.IRON_INGOT,
|
||||
"crystal_block", Rivencraft.CRYSTAL_SHARD.get(),
|
||||
"notebook", Items.LEATHER,
|
||||
"writing_desk", Items.FEATHER);
|
||||
for (var entry : materials.entrySet()) {
|
||||
var id = net.minecraft.resources.Identifier.fromNamespaceAndPath("rivencraft", entry.getKey());
|
||||
var key = net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE, id);
|
||||
var player = new PortProgressionPlayer(level, new com.mojang.authlib.GameProfile(
|
||||
java.util.UUID.nameUUIDFromBytes(id.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)),
|
||||
"RivenRecipe"));
|
||||
var advancement = level.getServer().getAdvancements().get(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("rivencraft", "recipes/misc/" + entry.getKey()));
|
||||
if (advancement == null || level.getServer().getRecipeManager().byKey(key).isEmpty())
|
||||
throw new IllegalStateException("Missing recipe/discovery data: " + id);
|
||||
var progress = player.getAdvancements();
|
||||
net.minecraft.advancements.triggers.CriteriaTriggers.INVENTORY_CHANGED.trigger(player,
|
||||
player.getInventory(), new ItemStack(Items.COBBLESTONE));
|
||||
if (player.getRecipeBook().contains(key))
|
||||
throw new IllegalStateException("Wrong material unlocked " + id);
|
||||
var material = new ItemStack(entry.getValue());
|
||||
player.getInventory().setItem(0, material);
|
||||
net.minecraft.advancements.triggers.CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), material);
|
||||
if (!player.getRecipeBook().contains(key) || !progress.getOrStartProgress(advancement).isDone())
|
||||
throw new IllegalStateException("Material did not unlock " + id);
|
||||
// The second criterion supports /recipe give without first obtaining its material.
|
||||
var other = new PortProgressionPlayer(level, new com.mojang.authlib.GameProfile(
|
||||
java.util.UUID.randomUUID(), "RivenRecipeGive"));
|
||||
if (other.getRecipeBook().contains(key)) throw new IllegalStateException("Recipe leaked between players");
|
||||
other.awardRecipes(java.util.List.of(level.getServer().getRecipeManager().byKey(key).orElseThrow()));
|
||||
if (!other.getAdvancements().getOrStartProgress(advancement).isDone())
|
||||
throw new IllegalStateException("Recipe unlock criterion did not complete " + id);
|
||||
}
|
||||
}
|
||||
|
||||
static void run(ServerLevel level) {
|
||||
var player = new PortProgressionPlayer(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("b428c3ef-57a6-43dc-95e8-441b6a171107"), "RivenProgression"));
|
||||
var pos = new BlockPos(4, 64, 0);
|
||||
player.setPos(4.5, 64, 2.5);
|
||||
level.setBlock(pos, Rivencraft.INK_MIXER.get().defaultBlockState(), 3);
|
||||
var mixer = (InkMixerBlockEntity) level.getBlockEntity(pos);
|
||||
try {
|
||||
// Every tree node must decode; portal advancement waits for its missing item/feature port.
|
||||
for (String path : new String[]{"root", "page", "linking_book", "link_panel", "copy_page", "bind_age", "enter_age"})
|
||||
if (isComplete(player, path))
|
||||
throw new IllegalStateException("Fixture requires fresh player advancement data");
|
||||
var menu = new InkMixerMenu(1, player.getInventory(), mixer);
|
||||
menu.clicked(InkMixerMenu.RESULT, 0, ContainerInput.PICKUP, player);
|
||||
if (isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Empty Mixer awarded progression");
|
||||
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
||||
mixer.setItem(InkMixerBlockEntity.INK, new ItemStack(Rivencraft.INK_VIAL.get()));
|
||||
menu.broadcastChanges();
|
||||
if (isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Mixer preview awarded progression");
|
||||
menu.clicked(InkMixerMenu.RESULT, 0, ContainerInput.PICKUP, player);
|
||||
if (!isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Mixer pickup did not award progression");
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
var advancement = level.getServer().getAdvancements().get(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("rivencraft", "progression/link_panel"));
|
||||
player.getAdvancements().revoke(advancement, "completed");
|
||||
menu.quickMoveStack(player, InkMixerMenu.RESULT);
|
||||
if (!isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Mixer shift pickup did not award progression");
|
||||
ProgressionAdvancements.awardAgeTravel(player);
|
||||
if (!isComplete(player, "enter_age"))
|
||||
throw new IllegalStateException("Registered Age did not award entry");
|
||||
} finally {
|
||||
player.getInventory().clearContent();
|
||||
level.removeBlock(pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public final class PortReturnLinks {
|
||||
boolean issue = shouldIssue(mode, storage.computeIfAbsent(PortAgeVisits.TYPE), age);
|
||||
if (!issue) return;
|
||||
ItemStack book = createBook(departure);
|
||||
var source = player.level().getServer().getLevel(net.minecraft.resources.ResourceKey.create(
|
||||
net.minecraft.core.registries.Registries.DIMENSION, departure.dimension()));
|
||||
if (source != null) book.set(Rivencraft.LINK_DESTINATION_NAME.get(),
|
||||
net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(source));
|
||||
if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
// Persist the once-per-Age marker before the next arrival can issue another.
|
||||
if (mode == PortConfig.ReturnLinkMode.ON_CREATION)
|
||||
|
||||
@@ -23,7 +23,7 @@ final class PortWritingDeskProbe {
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
var player = new PortProgressionPlayer(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("f92ad9ee-1ff0-4fe2-bcd3-994d4c035933"), "RivenDeskProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
// New Minecraft drops Container block-entity contents during removal.
|
||||
@@ -69,6 +69,8 @@ final class PortWritingDeskProbe {
|
||||
if (menu.getCarried().isEmpty() || desk.inkRemaining() != 15
|
||||
|| desk.getItem(WritingDeskBlockEntity.PAPER).getCount() != 1 || desk.getItem(0).isEmpty())
|
||||
throw new IllegalStateException("Desk copy did not consume exactly one paper/ink");
|
||||
if (!PortProgressionProbe.isComplete(player, "copy_page"))
|
||||
throw new IllegalStateException("Missing copy_page award");
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++)
|
||||
player.getInventory().setItem(slot, new ItemStack(Items.COBBLESTONE, 64));
|
||||
|
||||
@@ -270,6 +270,18 @@ public final class RuntimeAgeBackend {
|
||||
Rivencraft.LOGGER.info("Retired 26.3 prototype Age {} (archived at {})", id, paths.archive());
|
||||
}
|
||||
|
||||
/** Display metadata never changes the dimension identity, seed or filesystem path. */
|
||||
public static java.util.Optional<String> savedAgeName(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
return java.util.Optional.ofNullable(index(server).displayName(id));
|
||||
}
|
||||
|
||||
public static void setAgeDisplayName(MinecraftServer server, Identifier id, String name) {
|
||||
checkServerThread(server);
|
||||
index(server).setDisplayName(id, name);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
}
|
||||
|
||||
public static Map<Identifier, Long> savedAges(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).entries();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
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.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/** Vanilla lectern geometry with Rivencraft storage instead of a vanilla book menu. */
|
||||
public final class BookStandBlock extends LecternBlock {
|
||||
public BookStandBlock(Properties properties) { super(properties); }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new BookStandBlockEntity(pos, state); }
|
||||
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
|
||||
if (!(level.getBlockEntity(pos) instanceof BookStandBlockEntity stand)) return InteractionResult.PASS;
|
||||
if (player.isShiftKeyDown() && !level.isClientSide()) {
|
||||
var book = stand.takeBook();
|
||||
level.setBlock(pos, Blocks.LECTERN.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3);
|
||||
if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
level.playSound(null, pos, SoundEvents.BOOK_PAGE_TURN, SoundSource.BLOCKS, 1, 1);
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
@Override protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { return null; }
|
||||
@Override protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, net.minecraft.core.Direction direction) { return 0; }
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** One stored book or page, synchronized independently of the player's inventory. */
|
||||
public final class BookStandBlockEntity extends BlockEntity {
|
||||
private ItemStack book = ItemStack.EMPTY;
|
||||
public BookStandBlockEntity(BlockPos pos, BlockState state) { super(Rivencraft.BOOK_STAND_BLOCK_ENTITY.get(), pos, state); }
|
||||
public ItemStack getBook() { return book; }
|
||||
public void setBook(ItemStack stack) { book = stack; sync(); }
|
||||
public ItemStack takeBook() { ItemStack result = book; book = ItemStack.EMPTY; sync(); return result; }
|
||||
private void sync() {
|
||||
setChanged();
|
||||
if (level != null && !level.isClientSide()) level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 2);
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
book = input.read("Book", ItemStack.CODEC).orElse(ItemStack.EMPTY);
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
if (!book.isEmpty()) output.store("Book", ItemStack.CODEC, book);
|
||||
}
|
||||
@Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); }
|
||||
@Override public ClientboundBlockEntityDataPacket getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); }
|
||||
@Override public void preRemoveSideEffects(BlockPos pos, BlockState oldState) {
|
||||
if (level != null && !level.isClientSide())
|
||||
net.minecraft.world.Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5, takeBook());
|
||||
super.preRemoveSideEffects(pos, oldState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.client.model.object.book.BookModel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Extracted version of the 1.21.1 lectern display, retaining its final page orientation. */
|
||||
public final class BookStandRenderer implements BlockEntityRenderer<BookStandBlockEntity, BookStandRenderer.State> {
|
||||
private static final Identifier LINKING = Identifier.fromNamespaceAndPath("rivencraft", "textures/entity/linking_book.png");
|
||||
private static final Identifier DESCRIPTIVE = Identifier.fromNamespaceAndPath("rivencraft", "textures/entity/descriptive_book.png");
|
||||
private final BookModel model;
|
||||
private final ItemModelResolver resolver;
|
||||
private final Font font;
|
||||
public BookStandRenderer(BlockEntityRendererProvider.Context context) {
|
||||
model = new BookModel(context.bakeLayer(ModelLayers.BOOK));
|
||||
resolver = context.itemModelResolver();
|
||||
font = context.font();
|
||||
}
|
||||
public static final class State extends BlockEntityRenderState {
|
||||
final ItemStackRenderState page = new ItemStackRenderState();
|
||||
boolean present, isPage, descriptive;
|
||||
float rotation;
|
||||
Component name;
|
||||
}
|
||||
@Override public State createRenderState() { return new State(); }
|
||||
@Override public void extractRenderState(BookStandBlockEntity stand, State state, float partialTick,
|
||||
Vec3 camera, ModelFeatureRenderer.CrumblingOverlay overlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(stand, state, partialTick, camera, overlay);
|
||||
var book = stand.getBook();
|
||||
state.present = !book.isEmpty();
|
||||
state.isPage = book.is(Rivencraft.PAGE.get());
|
||||
state.descriptive = book.is(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
state.name = state.present && !state.isPage ? book.getHoverName().copy() : null;
|
||||
Direction facing = stand.getBlockState().getValue(BookStandBlock.FACING);
|
||||
state.rotation = state.isPage ? facing.toYRot() - 180 + (facing.getAxis() == Direction.Axis.X ? 180 : 0)
|
||||
: -facing.getClockWise().toYRot();
|
||||
resolver.updateForTopItem(state.page, state.isPage ? book : net.minecraft.world.item.ItemStack.EMPTY,
|
||||
ItemDisplayContext.FIXED, stand.getLevel(), null, 0);
|
||||
}
|
||||
@Override public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) {
|
||||
if (!state.present) return;
|
||||
pose.pushPose();
|
||||
pose.translate(0.5F, 1.0625F, 0.5F);
|
||||
pose.rotateDegrees(Axis.YP, state.rotation);
|
||||
if (state.isPage) {
|
||||
pose.rotateDegrees(Axis.XP, 67.5F);
|
||||
pose.scale(0.7F, 0.7F, 0.7F);
|
||||
state.page.submit(pose, collector, state.lightCoords, OverlayTexture.NO_OVERLAY, 0);
|
||||
} else {
|
||||
pose.rotateDegrees(Axis.ZP, 67.5F);
|
||||
pose.translate(0, -0.125F, 0);
|
||||
collector.submitModel(model, BookModel.State.forAnimation(0, 0.1F, 0.9F, 1.2F), pose,
|
||||
state.descriptive ? DESCRIPTIVE : LINKING, state.lightCoords, OverlayTexture.NO_OVERLAY, 0);
|
||||
}
|
||||
pose.popPose();
|
||||
if (state.name != null) {
|
||||
pose.pushPose();
|
||||
pose.translate(0.5F, 1.6F, 0.5F);
|
||||
pose.rotate(camera.orientation);
|
||||
pose.scale(0.025F, -0.025F, 0.025F);
|
||||
collector.submitText(pose, -font.width(state.name) / 2.0F, 0, state.name.getVisualOrderText(),
|
||||
false, Font.DisplayMode.NORMAL, state.lightCoords, 0xFFFFFFFF, 0, 0);
|
||||
pose.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -20,6 +20,7 @@ public final class PortBookBinderClientEvents {
|
||||
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
|
||||
event.registerEntityRenderer(Rivencraft.DROPPED_BOOK.get(), DroppedBookRenderer::new);
|
||||
event.registerBlockEntityRenderer(Rivencraft.WRITING_DESK_BLOCK_ENTITY.get(), WritingDeskRenderer::new);
|
||||
event.registerBlockEntityRenderer(Rivencraft.BOOK_STAND_BLOCK_ENTITY.get(), BookStandRenderer::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
||||
+15
@@ -26,4 +26,19 @@ public final class PortBookInteractionHandler {
|
||||
? new PortLinkingBookScreen(event.getHand(), event.getItemStack().copy())
|
||||
: new PortDescriptiveBookScreen(event.getHand(), event.getItemStack().copy()));
|
||||
}
|
||||
@SubscribeEvent
|
||||
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
if (!event.getLevel().isClientSide() || event.getEntity().isShiftKeyDown()
|
||||
|| !event.getItemStack().isEmpty()
|
||||
|| !(event.getLevel().getBlockEntity(event.getPos()) instanceof net.dnorberg.rivencraft.block.entity.BookStandBlockEntity stand)) return;
|
||||
var book = stand.getBook();
|
||||
if (!book.is(Rivencraft.LINKING_BOOK.get()) && !book.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
var source = new net.dnorberg.rivencraft.network.UseBookStandPayload(
|
||||
event.getLevel().dimension().identifier(), event.getPos(), book.copy());
|
||||
Minecraft.getInstance().setScreenAndShow(book.is(Rivencraft.LINKING_BOOK.get())
|
||||
? new PortLinkingBookScreen(source) : new PortDescriptiveBookScreen(source));
|
||||
}
|
||||
|
||||
}
|
||||
+88
-36
@@ -10,81 +10,111 @@ import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Temporary 26.3 descriptive-book interaction; authored pages and live previews remain unported. */
|
||||
/** Legacy book layout and authored glyph pages; destination rendering remains a port placeholder. */
|
||||
public final class PortDescriptiveBookScreen extends Screen {
|
||||
private static final int WIDTH = 327;
|
||||
private static final int HEIGHT = 199;
|
||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
|
||||
|
||||
private final InteractionHand hand;
|
||||
private final boolean bound;
|
||||
private final PortLegacyLinkPanel linkPanel = new PortLegacyLinkPanel();
|
||||
private final net.dnorberg.rivencraft.writing.DescriptiveBookPages pages;
|
||||
private int pageIndex;
|
||||
private net.dnorberg.rivencraft.network.UseBookStandPayload standSource;
|
||||
private EditBox titleInput;
|
||||
|
||||
public PortDescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
this.hand = hand;
|
||||
pages = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(book);
|
||||
bound = book.has(Rivencraft.AGE_BOOK_ID.get());
|
||||
}
|
||||
|
||||
public PortDescriptiveBookScreen(net.dnorberg.rivencraft.network.UseBookStandPayload source) {
|
||||
this(InteractionHand.MAIN_HAND, source.book());
|
||||
standSource = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) return;
|
||||
if (bound || standSource != null) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||
Component.literal("Age name"));
|
||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setHint(Component.literal("Name this Age"));
|
||||
titleInput.setHint(Component.literal(standSource == null ? "Name this Age" : "Remove book to bind"));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
|
||||
.bounds(left + 40, top + 100, 53, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||
.bounds(left + 97, top + 100, 53, 20).build());
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
||||
graphics.text(font, Component.literal("Age preview unavailable"), left + 181, top + 47,
|
||||
0xFFE9DFCB, false);
|
||||
graphics.text(font, Component.literal("Click to enter"), left + 205, top + 65,
|
||||
0xFFE9DFCB, false);
|
||||
} else {
|
||||
graphics.text(font, Component.literal("Bind a prototype Age"), left + 183, top + 39,
|
||||
TEXT_COLOR, false);
|
||||
PortLegacyBookArtwork.drawCover(graphics, left, top, true);
|
||||
if (pageIndex > 0) PortLegacyBookArtwork.drawLeftPage(graphics, left, top);
|
||||
if (pageIndex == 0) {
|
||||
if (bound) {
|
||||
graphics.nextStratum();
|
||||
linkPanel.render(graphics, left, top, partialTick);
|
||||
graphics.nextStratum();
|
||||
}
|
||||
PortLegacyBookArtwork.drawPanelPage(graphics, left, top);
|
||||
if (bound) {
|
||||
graphics.enableScissor(left + 38, top + 38, left + 154, top + 174);
|
||||
int textY = PortLegacyBookArtwork.drawText(graphics, font, title, left + 40, top + 40, 110, TEXT_COLOR);
|
||||
Component author = pages.author().isBlank()
|
||||
? Component.translatable("screen.rivencraft.age.unknown_author") : Component.literal(pages.author());
|
||||
PortLegacyBookArtwork.drawText(graphics, font, author, left + 50, textY + 2, 100, TEXT_COLOR);
|
||||
graphics.disableScissor();
|
||||
} else {
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
graphics.text(font, Component.literal(standSource == null ? "Bind a prototype Age" : "Remove book to bind"),
|
||||
left + 183, top + 39, TEXT_COLOR, false);
|
||||
}
|
||||
} else if (pages.symbolAt(pageIndex).isPresent()) {
|
||||
PortLegacyBookArtwork.drawFullPage(graphics, left, top);
|
||||
graphics.nextStratum();
|
||||
var symbol = pages.symbolAt(pageIndex).orElseThrow();
|
||||
ItemStack glyph = new ItemStack(Rivencraft.PAGE.get());
|
||||
glyph.set(Rivencraft.SYMBOL_PAGE_ID.get(), symbol);
|
||||
glyph.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.enableScissor(left + 174, top + 26, left + 306, top + 172);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(left + 177, top + 29);
|
||||
graphics.pose().scale(8.0F, 8.0F);
|
||||
graphics.fakeItem(glyph, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
graphics.disableScissor();
|
||||
}
|
||||
graphics.nextStratum();
|
||||
PortLegacyBookArtwork.drawCounter(graphics, font, left, top, pageIndex, pages.lastPage());
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
if (event.x() >= left + 173 && event.x() < left + 305
|
||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
||||
ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
int direction = PortLegacyBookArtwork.turnDirection(event.x() - left, event.y() - top, pageIndex, pages.lastPage());
|
||||
if (direction != 0) { turnPage(direction); return true; }
|
||||
if (bound && pageIndex == 0 && PortLegacyLinkPanel.contains(event.x() - left, event.y() - top)) {
|
||||
if (standSource != null) ClientPacketDistributor.sendToServer(standSource);
|
||||
else ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
@@ -92,6 +122,36 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
private void turnPage(int direction) {
|
||||
int next = pages.turn(pageIndex, direction);
|
||||
if (next == pageIndex) return;
|
||||
pageIndex = next;
|
||||
minecraft.getSoundManager().play(net.minecraft.client.resources.sounds.SimpleSoundInstance.forUI(
|
||||
net.minecraft.sounds.SoundEvents.BOOK_PAGE_TURN, 1.0F, 0.65F));
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
private void updatePageControls() {
|
||||
for (var widget : renderables) if (widget instanceof net.minecraft.client.gui.components.AbstractWidget control)
|
||||
control.visible = pageIndex == 0;
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(net.minecraft.client.input.KeyEvent event) {
|
||||
if (event.isEscape()) return super.keyPressed(event);
|
||||
if (titleInput == null || !titleInput.isFocused() || !titleInput.visible) {
|
||||
if (event.isLeft()) { turnPage(-1); return true; }
|
||||
if (event.isRight()) { turnPage(1); return true; }
|
||||
} else {
|
||||
// Keep inventory/hotbar actions out of a focused name field.
|
||||
return titleInput.keyPressed(event);
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override public void tick() {
|
||||
if (bound) linkPanel.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
@@ -105,12 +165,4 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
onClose();
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Original Mystcraft Legacy book atlases and page composition (LGPL-3.0,
|
||||
* copyright XCompWiz, revision 9bc8ddc061845df0cd5ea47f4fdef773cff786ae). */
|
||||
final class PortLegacyBookArtwork {
|
||||
private static final int ATLAS_SIZE = 256;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
private static final Identifier RIGHT_FULL = texture("bookui_rpage_full.png");
|
||||
|
||||
private PortLegacyBookArtwork() {}
|
||||
|
||||
static void drawCover(GuiGraphicsExtractor graphics, int left, int top, boolean gilded) {
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
if (gilded) {
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 186, 0);
|
||||
blit(graphics, COVER, left + 293, top + 7, 34, 192, 186, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawLeftPage(GuiGraphicsExtractor graphics, int left, int top) {
|
||||
blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
}
|
||||
|
||||
static void drawPanelPage(GuiGraphicsExtractor graphics, int left, int top) {
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
}
|
||||
|
||||
static void drawFullPage(GuiGraphicsExtractor graphics, int left, int top) {
|
||||
blit(graphics, RIGHT_FULL, left + 163, top, 156, 195, 0, 0);
|
||||
}
|
||||
|
||||
static int drawText(GuiGraphicsExtractor graphics, net.minecraft.client.gui.Font font,
|
||||
net.minecraft.network.chat.Component text, int x, int y, int width, int color) {
|
||||
for (var line : font.split(text, width)) {
|
||||
graphics.text(font, line, x, y, color, false);
|
||||
y += font.lineHeight;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
static void drawCounter(GuiGraphicsExtractor graphics, net.minecraft.client.gui.Font font,
|
||||
int left, int top, int page, int last) {
|
||||
String counter = page + "/" + last;
|
||||
graphics.text(font, counter, left + 165 - font.width(counter) / 2, top + 185, 0xFF000000, false);
|
||||
}
|
||||
|
||||
static int turnDirection(double x, double y, int page, int last) {
|
||||
if (y < 164 || y >= 195) return 0;
|
||||
if (page > 0 && x >= 7 && x < 35) return -1;
|
||||
return page < last && x >= 293 && x < 320 ? 1 : 0;
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath("rivencraft", "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, ATLAS_SIZE, ATLAS_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
/**
|
||||
* Built-in linking-panel animation inspired by Mystcraft Legacy's portal bloom.
|
||||
*
|
||||
* <p>The original effect used a GLSL noise shader over an optional live world
|
||||
* view. This port retains the 1.21.1 fallback's two noisy spirals and default teal
|
||||
* tint, using the normal GUI API without a renderer mod.</p>
|
||||
*/
|
||||
final class PortLegacyLinkPanel {
|
||||
static final int X = 173, Y = 20, WIDTH = 132, HEIGHT = 83;
|
||||
private static final int CELL_SIZE = 3;
|
||||
private static final int BORDER_COLOR = 0xFF7B6AA8;
|
||||
private int ticks;
|
||||
private static final int LINK_COLOR = 0xFF006666;
|
||||
|
||||
static boolean contains(double x, double y) {
|
||||
return x >= X && x < X + WIDTH && y >= Y && y < Y + HEIGHT;
|
||||
}
|
||||
|
||||
void render(GuiGraphicsExtractor graphics, int bookLeft, int bookTop, float partialTick) {
|
||||
render(graphics, bookLeft + X, bookTop + Y, WIDTH, HEIGHT, partialTick);
|
||||
}
|
||||
|
||||
void tick() {
|
||||
ticks++;
|
||||
}
|
||||
|
||||
private void render(GuiGraphicsExtractor graphics, int left, int top, int width, int height, float partialTick) {
|
||||
graphics.fill(left, top, left + width, top + height, 0xFF000011);
|
||||
|
||||
float time = (ticks + partialTick) * 0.035F;
|
||||
float reveal = Math.min(1.0F, time / 1.8F);
|
||||
for (int y = 1; y < height - 1; y += CELL_SIZE) {
|
||||
for (int x = 1; x < width - 1; x += CELL_SIZE) {
|
||||
float normalizedX = (x - width * 0.5F) / height;
|
||||
float normalizedY = (y - height * 0.5F) / height;
|
||||
float first = spiral(normalizedX, normalizedY, time, 0.0F);
|
||||
float second = spiral(normalizedX, normalizedY, time, 2.35F);
|
||||
float intensity = Math.max(first, second) * reveal;
|
||||
if (intensity <= 0.035F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int color = scaledColor(LINK_COLOR, Math.min(1.0F, 0.12F + intensity));
|
||||
graphics.fill(left + x, top + y,
|
||||
left + Math.min(width - 1, x + CELL_SIZE),
|
||||
top + Math.min(height - 1, y + CELL_SIZE), color);
|
||||
}
|
||||
}
|
||||
graphics.outline(left, top, width, height, BORDER_COLOR);
|
||||
}
|
||||
|
||||
private static float spiral(float x, float y, float time, float phase) {
|
||||
float distance = (float)Math.sqrt(x * x + y * y);
|
||||
float angle = (float)Math.atan2(y, x);
|
||||
float arm = 0.5F + 0.5F * (float)Math.cos(angle * 3.0F - distance * 25.0F + time * 4.0F + phase);
|
||||
float pulse = 0.72F + 0.28F * noise(x * 31.0F, y * 31.0F, time + phase);
|
||||
float core = Math.max(0.0F, 1.0F - distance * 2.15F);
|
||||
return core * (0.28F + arm * 0.72F) * pulse;
|
||||
}
|
||||
|
||||
private static float noise(float x, float y, float time) {
|
||||
double value = Math.sin(x * 12.9898D + y * 78.233D + time * 19.19D) * 43758.5453D;
|
||||
return (float)(value - Math.floor(value));
|
||||
}
|
||||
|
||||
private static int scaledColor(int color, float scale) {
|
||||
int red = Math.min(255, Math.round(((color >> 16) & 0xFF) * scale + 18.0F * scale));
|
||||
int green = Math.min(255, Math.round(((color >> 8) & 0xFF) * scale + 24.0F * scale));
|
||||
int blue = Math.min(255, Math.round((color & 0xFF) * scale + 42.0F * scale));
|
||||
return 0xFF000000 | red << 16 | green << 8 | blue;
|
||||
}
|
||||
}
|
||||
+82
-33
@@ -10,35 +10,48 @@ import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Minimal 26.3 book interaction using the Legacy artwork; live preview is not ported yet. */
|
||||
/** Legacy Linking Book front and final spreads with the shared fallback panel. */
|
||||
public final class PortLinkingBookScreen extends Screen {
|
||||
private static final int WIDTH = 327;
|
||||
private static final int HEIGHT = 199;
|
||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
|
||||
|
||||
private final InteractionHand hand;
|
||||
private final boolean bound;
|
||||
private final net.dnorberg.rivencraft.item.LinkBookTarget target;
|
||||
private final boolean returnLink;
|
||||
private final Component destination;
|
||||
private int pageIndex;
|
||||
private final PortLegacyLinkPanel linkPanel = new PortLegacyLinkPanel();
|
||||
private net.dnorberg.rivencraft.network.UseBookStandPayload standSource;
|
||||
private EditBox titleInput;
|
||||
|
||||
public PortLinkingBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
this.hand = hand;
|
||||
bound = book.has(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
target = book.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
bound = target != null;
|
||||
destination = bound ? net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(book, target) : Component.empty();
|
||||
Component customName = book.get(net.minecraft.core.component.DataComponents.CUSTOM_NAME);
|
||||
returnLink = customName != null
|
||||
&& customName.getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents translated
|
||||
&& translated.getKey().equals("item.rivencraft.linking_book.return");
|
||||
}
|
||||
|
||||
public PortLinkingBookScreen(net.dnorberg.rivencraft.network.UseBookStandPayload source) {
|
||||
this(InteractionHand.MAIN_HAND, source.book());
|
||||
standSource = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) return;
|
||||
if (bound || standSource != null) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||
@@ -51,38 +64,55 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
.bounds(left + 40, top + 100, 53, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||
.bounds(left + 97, top + 100, 53, 20).build());
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
if (bound) blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
||||
graphics.text(font, Component.literal("Destination preview"), left + 185, top + 47, 0xFFE9DFCB, false);
|
||||
graphics.text(font, Component.literal("Click to link"), left + 205, top + 65, 0xFFE9DFCB, false);
|
||||
PortLegacyBookArtwork.drawCover(graphics, left, top, false);
|
||||
if (pageIndex > 0) {
|
||||
PortLegacyBookArtwork.drawLeftPage(graphics, left, top);
|
||||
} else {
|
||||
graphics.text(font, Component.literal("Name this Linking Book"), left + 183, top + 39,
|
||||
TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.nextStratum();
|
||||
linkPanel.render(graphics, left, top, partialTick);
|
||||
graphics.nextStratum();
|
||||
}
|
||||
PortLegacyBookArtwork.drawPanelPage(graphics, left, top);
|
||||
graphics.enableScissor(left + 38, top + 38, left + 154, top + 174);
|
||||
if (bound) {
|
||||
int y = top + 40;
|
||||
if (!returnLink) y = PortLegacyBookArtwork.drawText(graphics, font, title,
|
||||
left + 40, y, 110, TEXT_COLOR) + 4;
|
||||
y = PortLegacyBookArtwork.drawText(graphics, font, destination, left + 50, y, 100, TEXT_COLOR) + 10;
|
||||
var pos = net.minecraft.core.BlockPos.containing(target.x(), target.y(), target.z());
|
||||
PortLegacyBookArtwork.drawText(graphics, font,
|
||||
Component.literal(pos.getX() + ", " + pos.getY() + ", " + pos.getZ()),
|
||||
left + 40, y, 110, TEXT_COLOR);
|
||||
} else {
|
||||
PortLegacyBookArtwork.drawText(graphics, font, Component.literal(
|
||||
standSource == null ? "Name this Linking Book" : "Remove book to bind"),
|
||||
left + 40, top + 43, 110, TEXT_COLOR);
|
||||
}
|
||||
graphics.disableScissor();
|
||||
}
|
||||
graphics.nextStratum();
|
||||
PortLegacyBookArtwork.drawCounter(graphics, font, left, top, pageIndex, 1);
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
if (event.x() >= left + 173 && event.x() < left + 305
|
||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
||||
ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
int direction = PortLegacyBookArtwork.turnDirection(event.x() - left, event.y() - top, pageIndex, 1);
|
||||
if (direction != 0) { turnPage(direction); return true; }
|
||||
if (bound && pageIndex == 0 && PortLegacyLinkPanel.contains(event.x() - left, event.y() - top)) {
|
||||
if (standSource != null) ClientPacketDistributor.sendToServer(standSource);
|
||||
else ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
@@ -90,6 +120,33 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
private void turnPage(int direction) {
|
||||
int next = Math.clamp(pageIndex + direction, 0, 1);
|
||||
if (next == pageIndex) return;
|
||||
pageIndex = next;
|
||||
minecraft.getSoundManager().play(net.minecraft.client.resources.sounds.SimpleSoundInstance.forUI(
|
||||
net.minecraft.sounds.SoundEvents.BOOK_PAGE_TURN, 1.0F, 0.65F));
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
private void updatePageControls() {
|
||||
for (var widget : renderables) if (widget instanceof net.minecraft.client.gui.components.AbstractWidget control)
|
||||
control.visible = pageIndex == 0;
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(net.minecraft.client.input.KeyEvent event) {
|
||||
if (event.isEscape()) return super.keyPressed(event);
|
||||
if (titleInput == null || !titleInput.isFocused() || !titleInput.visible) {
|
||||
if (event.isLeft()) { turnPage(-1); return true; }
|
||||
if (event.isRight()) { turnPage(1); return true; }
|
||||
} else return titleInput.keyPressed(event);
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override public void tick() {
|
||||
if (bound) linkPanel.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
@@ -103,12 +160,4 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
onClose();
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,17 @@ public final class DroppedBookEntity extends ItemEntity {
|
||||
setPickUpDelay(20);
|
||||
}
|
||||
|
||||
/** Conversion preserves ordinary toss/drop state; the travel constructor has different placement rules. */
|
||||
public static DroppedBookEntity fromDrop(ItemEntity source) {
|
||||
var book = new DroppedBookEntity(Rivencraft.DROPPED_BOOK.get(), source.level());
|
||||
book.restoreFrom(source);
|
||||
// The original remains registered until replacement succeeds, so this needs a new identity.
|
||||
book.setUUID(java.util.UUID.randomUUID());
|
||||
book.setItem(source.getItem().copy());
|
||||
book.setUnlimitedLifetime();
|
||||
return book;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.event;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
|
||||
public final class BookStandConversionHandler {
|
||||
private BookStandConversionHandler() {}
|
||||
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
var book = event.getItemStack();
|
||||
if (!book.is(Rivencraft.LINKING_BOOK.get()) && !book.is(Rivencraft.DESCRIPTIVE_BOOK.get()) && !book.is(Rivencraft.PAGE.get())) return;
|
||||
var state = event.getLevel().getBlockState(event.getPos());
|
||||
if (!state.is(Blocks.LECTERN) || state.getValue(LecternBlock.HAS_BOOK)) return;
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
if (!event.getLevel().isClientSide()) convert(event.getEntity(), event.getHand(), event.getPos());
|
||||
}
|
||||
public static boolean convert(Player player, InteractionHand hand, BlockPos pos) {
|
||||
var level = player.level();
|
||||
var state = level.getBlockState(pos);
|
||||
var book = player.getItemInHand(hand);
|
||||
if (level.isClientSide() || !state.is(Blocks.LECTERN) || state.getValue(LecternBlock.HAS_BOOK)
|
||||
|| (!book.is(Rivencraft.LINKING_BOOK.get()) && !book.is(Rivencraft.DESCRIPTIVE_BOOK.get()) && !book.is(Rivencraft.PAGE.get()))) return false;
|
||||
if (!level.setBlock(pos, Rivencraft.BOOK_STAND.get().defaultBlockState()
|
||||
.setValue(BookStandBlock.FACING, state.getValue(LecternBlock.FACING)).setValue(BookStandBlock.HAS_BOOK, true), 3)) return false;
|
||||
if (!(level.getBlockEntity(pos) instanceof BookStandBlockEntity stand)) return false;
|
||||
stand.setBook(book.consumeAndReturn(1, player));
|
||||
level.playSound(null, pos, SoundEvents.BOOK_PUT, SoundSource.BLOCKS, 1, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package net.dnorberg.rivencraft.event;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.server.TickTask;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.bus.api.EventPriority;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent;
|
||||
|
||||
/** Converts ordinary book drops from every source, including entities restored from disk. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID)
|
||||
public final class DroppedBookConversionHandler {
|
||||
private DroppedBookConversionHandler() {}
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.LOWEST)
|
||||
public static void onJoin(EntityJoinLevelEvent event) {
|
||||
if (!(event.getLevel() instanceof ServerLevel level)
|
||||
|| !(event.getEntity() instanceof ItemEntity item) || item instanceof DroppedBookEntity
|
||||
|| !isBook(item.getItem())) return;
|
||||
// Join events can run before chunk promotion. Do not add another entity inside that callback.
|
||||
// Keep the original until the queued replacement has actually been accepted.
|
||||
level.getServer().schedule(new TickTask(0, () -> {
|
||||
if (level.getServer().getLevel(level.dimension()) != level || item.isRemoved()
|
||||
|| !item.isAddedToLevel() || item.level() != level || !isBook(item.getItem())) return;
|
||||
var replacement = DroppedBookEntity.fromDrop(item);
|
||||
if (level.addFreshEntity(replacement)) item.discard();
|
||||
}));
|
||||
}
|
||||
|
||||
private static boolean isBook(ItemStack stack) {
|
||||
return stack.is(Rivencraft.LINKING_BOOK.get()) || stack.is(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
}
|
||||
}
|
||||
@@ -102,13 +102,37 @@ public final class LinkingBookItem extends Item {
|
||||
}
|
||||
}
|
||||
|
||||
public static String destinationName(ServerLevel level) {
|
||||
var id = level.dimension().identifier();
|
||||
return net.dnorberg.rivencraft.age.RuntimeAgeBackend.savedAgeName(level.getServer(), id)
|
||||
.orElseGet(() -> {
|
||||
if (id.equals(Level.OVERWORLD.identifier())) return "Overworld";
|
||||
if (id.equals(Level.NETHER.identifier())) return "The Nether";
|
||||
if (id.equals(Level.END.identifier())) return "The End";
|
||||
return id.toString();
|
||||
});
|
||||
}
|
||||
|
||||
public static Component destinationName(ItemStack book, LinkBookTarget target) {
|
||||
String name = book.get(Rivencraft.LINK_DESTINATION_NAME.get());
|
||||
if (name != null && !name.isBlank()) return Component.literal(name);
|
||||
var id = target.dimension();
|
||||
if (id.equals(Level.OVERWORLD.identifier())) return Component.translatable("dimension.minecraft.overworld");
|
||||
if (id.equals(Level.NETHER.identifier())) return Component.translatable("dimension.minecraft.the_nether");
|
||||
if (id.equals(Level.END.identifier())) return Component.translatable("dimension.minecraft.the_end");
|
||||
return Component.literal(id.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
LinkBookTarget target = stack.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
tooltip.accept(Component.literal(target == null ? "Unbound (26.3 port preview)"
|
||||
: "Destination: " + target.dimension() + " (" + (int) target.x() + ", "
|
||||
+ (int) target.y() + ", " + (int) target.z() + ")"));
|
||||
if (target == null) tooltip.accept(Component.literal("Unbound (26.3 port preview)"));
|
||||
else {
|
||||
BlockPos pos = BlockPos.containing(target.x(), target.y(), target.z());
|
||||
tooltip.accept(Component.translatable("item.rivencraft.linking_book.destination",
|
||||
destinationName(stack, target).getString(), pos.getX(), pos.getY(), pos.getZ()));
|
||||
}
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
|
||||
@@ -21,6 +21,7 @@ public final class PortHandheldTravel {
|
||||
try {
|
||||
String failure = travel.get();
|
||||
if (failure != null) return failure;
|
||||
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||
dropped = source.addFreshEntity(new DroppedBookEntity(source, departure, book));
|
||||
return null;
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.advancement.ProgressionAdvancements;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.Container;
|
||||
@@ -110,6 +112,8 @@ public final class InkMixerMenu extends AbstractContainerMenu {
|
||||
if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty())
|
||||
return ItemStack.EMPTY;
|
||||
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.LINK_PANEL);
|
||||
refreshResult();
|
||||
return output;
|
||||
}
|
||||
@@ -150,6 +154,8 @@ public final class InkMixerMenu extends AbstractContainerMenu {
|
||||
}
|
||||
@Override public void onTake(Player player, ItemStack stack) {
|
||||
if (!clientSide && mixer.consumeLinkPanelMaterials()) {
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.LINK_PANEL);
|
||||
refreshResult();
|
||||
}
|
||||
super.onTake(player, stack);
|
||||
|
||||
@@ -118,6 +118,7 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
||||
if (id == null) return;
|
||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
book.set(Rivencraft.BOOK_AUTHOR.get(), player.getGameProfile().name());
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
if (blockEntity.hasSymbolPages())
|
||||
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
||||
@@ -129,6 +130,8 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
||||
blockEntity.clearSymbolPages();
|
||||
setCarried(book);
|
||||
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.award(player,
|
||||
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.BIND_AGE);
|
||||
broadcastChanges();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.advancement.ProgressionAdvancements;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
@@ -321,6 +323,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
if (!blockEntity.consumeCopyMaterials()) return ItemStack.EMPTY;
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||
refreshOutput();
|
||||
return copy;
|
||||
}
|
||||
@@ -439,6 +443,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
}
|
||||
@Override public void onTake(Player player, ItemStack stack) {
|
||||
if (!clientSide && blockEntity.consumeCopyMaterials()) {
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||
refreshOutput();
|
||||
}
|
||||
super.onTake(player, stack);
|
||||
|
||||
@@ -38,6 +38,10 @@ public final class PortBookNetworking {
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) bind(player, payload);
|
||||
});
|
||||
registrar.playToServer(UseBookStandPayload.TYPE, UseBookStandPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) useStand(player, payload);
|
||||
});
|
||||
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) use(player, payload);
|
||||
@@ -73,6 +77,7 @@ public final class PortBookNetworking {
|
||||
String title = payload.title().strip();
|
||||
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), LinkBookTarget.at(player));
|
||||
stack.set(Rivencraft.LINK_DESTINATION_NAME.get(), LinkingBookItem.destinationName(player.level()));
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
@@ -96,6 +101,7 @@ public final class PortBookNetworking {
|
||||
Identifier id = createAge(player, title);
|
||||
if (id == null) return;
|
||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
stack.set(Rivencraft.BOOK_AUTHOR.get(), player.getGameProfile().name());
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
@@ -157,6 +163,7 @@ public final class PortBookNetworking {
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
|
||||
net.dnorberg.rivencraft.age.PortAcceleratedTicks.configure(level,
|
||||
net.dnorberg.rivencraft.age.PortAcceleratedTicks.pageCount(pages));
|
||||
RuntimeAgeBackend.setAgeDisplayName(server, id, title);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return null;
|
||||
@@ -167,22 +174,51 @@ public final class PortBookNetworking {
|
||||
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
||||
travelDescriptive(player, stack, hand(payload.offhand()));
|
||||
}
|
||||
|
||||
/** Validation is shared with operator fixtures; no remote chunk is loaded to inspect a stand. */
|
||||
public static ItemStack validatedStandBook(ServerPlayer player, UseBookStandPayload payload) {
|
||||
var level = player.level();
|
||||
if (!level.dimension().identifier().equals(payload.dimension())
|
||||
|| player.distanceToSqr(Vec3.atCenterOf(payload.pos())) > 64
|
||||
|| !level.hasChunkAt(payload.pos())
|
||||
|| !level.getBlockState(payload.pos()).is(Rivencraft.BOOK_STAND.get())
|
||||
|| !(level.getBlockEntity(payload.pos()) instanceof net.dnorberg.rivencraft.block.entity.BookStandBlockEntity stand)
|
||||
|| !ItemStack.matches(stand.getBook(), payload.book())) return ItemStack.EMPTY;
|
||||
return stand.getBook();
|
||||
}
|
||||
|
||||
private static void useStand(ServerPlayer player, UseBookStandPayload payload) {
|
||||
var book = validatedStandBook(player, payload);
|
||||
if (book.is(Rivencraft.LINKING_BOOK.get())) {
|
||||
String failure = LinkingBookItem.travel(player, book);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
else net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||
} else if (book.is(Rivencraft.DESCRIPTIVE_BOOK.get())) travelDescriptive(player, book, null);
|
||||
}
|
||||
|
||||
private static void travelDescriptive(ServerPlayer player, ItemStack stack, InteractionHand heldHand) {
|
||||
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (id == null) return;
|
||||
ServerLevel source = player.level();
|
||||
LinkBookTarget returnTarget = LinkBookTarget.at(player);
|
||||
Vec3 departure = player.position();
|
||||
Vec3 incoming = player.getDeltaMovement();
|
||||
String failure = net.dnorberg.rivencraft.item.PortHandheldTravel.travel(player,
|
||||
hand(payload.offhand()), stack, () -> {
|
||||
String result = PortAgeTravel.enter(player, id);
|
||||
if (result == null) LinkingBookItem.applySuccessfulLink(player,
|
||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||
source, departure, incoming, player.getYRot());
|
||||
return result;
|
||||
});
|
||||
java.util.function.Supplier<String> travel = () -> {
|
||||
String result = PortAgeTravel.enter(player, id);
|
||||
if (result == null) LinkingBookItem.applySuccessfulLink(player,
|
||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||
source, departure, incoming, player.getYRot());
|
||||
return result;
|
||||
};
|
||||
String failure = heldHand == null ? travel.get()
|
||||
: net.dnorberg.rivencraft.item.PortHandheldTravel.travel(player, heldHand, stack, travel);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
else net.dnorberg.rivencraft.age.PortReturnLinks.afterSuccessfulTravel(player, id, returnTarget);
|
||||
else {
|
||||
if (heldHand == null) net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||
net.dnorberg.rivencraft.age.PortReturnLinks.afterSuccessfulTravel(player, id, returnTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private static InteractionHand hand(boolean offhand) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Carries the displayed source so a removed/replaced book cannot activate a stale screen. */
|
||||
public record UseBookStandPayload(Identifier dimension, BlockPos pos, ItemStack book) implements CustomPacketPayload {
|
||||
public static final Type<UseBookStandPayload> TYPE = new Type<>(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_book_stand"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, UseBookStandPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
Identifier.STREAM_CODEC, UseBookStandPayload::dimension, BlockPos.STREAM_CODEC, UseBookStandPayload::pos,
|
||||
ItemStack.STREAM_CODEC, UseBookStandPayload::book, UseBookStandPayload::new);
|
||||
@Override public Type<UseBookStandPayload> type() { return TYPE; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Client-readable snapshot of authored pages; index zero is the front/link-panel spread. */
|
||||
public record DescriptiveBookPages(List<Identifier> symbols, String author) {
|
||||
public DescriptiveBookPages { symbols = List.copyOf(symbols); }
|
||||
public static DescriptiveBookPages from(ItemStack book) {
|
||||
return new DescriptiveBookPages(book.getOrDefault(Rivencraft.BOOK_SYMBOLS.get(), List.of()),
|
||||
book.getOrDefault(Rivencraft.BOOK_AUTHOR.get(), ""));
|
||||
}
|
||||
public int lastPage() { return Math.max(1, symbols.size()); }
|
||||
public int turn(int page, int direction) { return Math.clamp(page + direction, 0, lastPage()); }
|
||||
public Optional<Identifier> symbolAt(int page) {
|
||||
return page >= 1 && page <= symbols.size() ? Optional.of(symbols.get(page - 1)) : Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"display": {
|
||||
"icon": {
|
||||
"id": "rivencraft:descriptive_book"
|
||||
},
|
||||
"title": {
|
||||
"translate": "advancement.rivencraft.root.title"
|
||||
},
|
||||
"description": {
|
||||
"translate": "advancement.rivencraft.root.description"
|
||||
},
|
||||
"background": "rivencraft:block/crystal_block",
|
||||
"show_toast": false,
|
||||
"announce_to_chat": false
|
||||
},
|
||||
"criteria": {
|
||||
"completed": {
|
||||
"trigger": "minecraft:tick"
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_material": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:iron_ingot"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:book_binder"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_material",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:book_binder"
|
||||
]
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_crystal_shard": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "rivencraft:crystal_shard"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:crystal_block"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_crystal_shard",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:crystal_block"
|
||||
]
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_material": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:glass_bottle"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_mixer"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_material",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_mixer"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_black_dye": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:black_dye"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_vial"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_black_dye",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_vial"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user