Public Access
1 parent
db768bb4f7
commit
55c581bf01
12 files changed
+317
-6
No files matched your search
@@ -4,10 +4,19 @@ Rivencraft is a clean, incremental port of the ideas and gameplay of
|
||||
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft
|
||||
1.21.1 on NeoForge.
|
||||
|
||||
The bootstrap milestone is complete. The project currently provides a valid
|
||||
NeoForge mod, a creative tab, and an unwritten Descriptive Book item. Age
|
||||
generation, linking, symbols, instability, and writing mechanics have not been
|
||||
ported yet.
|
||||
The bootstrap milestone is complete and books/linking is in progress. The
|
||||
project currently provides an unwritten Descriptive Book and a functional
|
||||
Linking Book with persistent, dimension-aware destinations and safe landing
|
||||
checks. Age generation, symbols, instability, and writing mechanics have not
|
||||
been ported yet.
|
||||
|
||||
## Linking Book
|
||||
|
||||
Craft a Linking Book from a vanilla book and an ender pearl. Sneak-right-click
|
||||
the unbound book to record the current location, then right-click it to return
|
||||
there. The destination is stored on the item and survives inventory and world
|
||||
saves. Travel only succeeds if the target dimension is loaded and a solid,
|
||||
collision-free landing position can be found nearby.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
+17
-1
@@ -10,7 +10,7 @@ split into independently testable milestones.
|
||||
1. **Bootstrap** — NeoForge 1.21.1 build, mod metadata, item registration, and
|
||||
CI. **Complete.**
|
||||
2. **Books and linking** — descriptive/linking book data components, lectern-like
|
||||
interaction, safe teleportation, and return links.
|
||||
interaction, safe teleportation, and return links. **In progress.**
|
||||
3. **Ages** — dimension definitions backed by data packs, deterministic seeds,
|
||||
lifecycle management, and persistence.
|
||||
4. **Writing system** — pages, symbols, notebooks, desks, and book assembly.
|
||||
@@ -37,6 +37,22 @@ server-managed Age definition, travel to that Age, and use a Linking Book to
|
||||
return. This exercises persistence, networking, dimensions, and safety before
|
||||
the symbol grammar is introduced.
|
||||
|
||||
### Milestone 2 progress
|
||||
|
||||
- [x] Persistent, network-synchronized link-target data component.
|
||||
- [x] Linking Book binding with dimension, block position, yaw, and pitch.
|
||||
- [x] Server-authoritative cross-dimension travel with world-border, collision,
|
||||
fluid, and floor checks.
|
||||
- [x] Nearby landing search and clear failure messages.
|
||||
- [ ] Descriptive Book identity and Age reference components.
|
||||
- [ ] Return-link creation as part of Descriptive Book travel.
|
||||
- [ ] Lectern/book-stand interaction.
|
||||
- [ ] Automated in-game tests for binding, persistence, and travel.
|
||||
|
||||
The temporary shapeless Linking Book recipe makes the linking subsystem easy
|
||||
to test. It can move into the writing/bookbinder progression when that system
|
||||
is implemented.
|
||||
|
||||
## Milestone 1 acceptance record
|
||||
|
||||
- The project builds on a Java 21 JDK with `./gradlew build`.
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
@@ -13,6 +14,7 @@ public final class Rivencraft {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
public Rivencraft(IEventBus modEventBus) {
|
||||
ModDataComponents.register(modEventBus);
|
||||
ModItems.register(modEventBus);
|
||||
ModCreativeTabs.register(modEventBus);
|
||||
LOGGER.info("Initializing Rivencraft");
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.core.GlobalPos;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** A persistent, dimension-aware destination recorded in a Linking Book. */
|
||||
public record LinkTarget(GlobalPos location, float yaw, float pitch) {
|
||||
public static final Codec<LinkTarget> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
GlobalPos.CODEC.fieldOf("location").forGetter(LinkTarget::location),
|
||||
Codec.FLOAT.fieldOf("yaw").forGetter(LinkTarget::yaw),
|
||||
Codec.FLOAT.fieldOf("pitch").forGetter(LinkTarget::pitch)
|
||||
).apply(instance, LinkTarget::new));
|
||||
|
||||
public static final StreamCodec<ByteBuf, LinkTarget> STREAM_CODEC = StreamCodec.composite(
|
||||
GlobalPos.STREAM_CODEC,
|
||||
LinkTarget::location,
|
||||
ByteBufCodecs.FLOAT,
|
||||
LinkTarget::yaw,
|
||||
ByteBufCodecs.FLOAT,
|
||||
LinkTarget::pitch,
|
||||
LinkTarget::new
|
||||
);
|
||||
|
||||
public static LinkTarget from(ServerPlayer player) {
|
||||
return new LinkTarget(
|
||||
GlobalPos.of(player.level().dimension(), player.blockPosition()),
|
||||
player.getYRot(),
|
||||
player.getXRot()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.border.WorldBorder;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Server-side validation and execution for book travel. */
|
||||
public final class LinkingService {
|
||||
private static final int HORIZONTAL_SEARCH_RADIUS = 4;
|
||||
private static final int VERTICAL_SEARCH_RADIUS = 2;
|
||||
|
||||
private LinkingService() {
|
||||
}
|
||||
|
||||
public static LinkResult travel(ServerPlayer player, LinkTarget target) {
|
||||
ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension());
|
||||
if (destinationLevel == null) {
|
||||
return LinkResult.MISSING_DIMENSION;
|
||||
}
|
||||
|
||||
Optional<Vec3> destination = findSafeDestination(destinationLevel, target.location().pos());
|
||||
if (destination.isEmpty()) {
|
||||
return LinkResult.NO_SAFE_DESTINATION;
|
||||
}
|
||||
|
||||
Vec3 position = destination.get();
|
||||
player.teleportTo(
|
||||
destinationLevel,
|
||||
position.x,
|
||||
position.y,
|
||||
position.z,
|
||||
target.yaw(),
|
||||
target.pitch()
|
||||
);
|
||||
player.resetFallDistance();
|
||||
return LinkResult.SUCCESS;
|
||||
}
|
||||
|
||||
static Optional<Vec3> findSafeDestination(ServerLevel level, BlockPos origin) {
|
||||
WorldBorder border = level.getWorldBorder();
|
||||
return candidates(origin)
|
||||
.filter(level::isInWorldBounds)
|
||||
.filter(border::isWithinBounds)
|
||||
.filter(position -> isSafe(level, position))
|
||||
.map(position -> Vec3.atBottomCenterOf(position))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private static Stream<BlockPos> candidates(BlockPos origin) {
|
||||
return BlockPos.betweenClosedStream(
|
||||
origin.offset(-HORIZONTAL_SEARCH_RADIUS, -VERTICAL_SEARCH_RADIUS, -HORIZONTAL_SEARCH_RADIUS),
|
||||
origin.offset(HORIZONTAL_SEARCH_RADIUS, VERTICAL_SEARCH_RADIUS, HORIZONTAL_SEARCH_RADIUS)
|
||||
)
|
||||
.map(BlockPos::immutable)
|
||||
.sorted(Comparator.comparingDouble(position -> position.distSqr(origin)));
|
||||
}
|
||||
|
||||
private static boolean isSafe(ServerLevel level, BlockPos feet) {
|
||||
BlockPos head = feet.above();
|
||||
BlockPos floor = feet.below();
|
||||
return level.getBlockState(feet).getCollisionShape(level, feet).isEmpty()
|
||||
&& level.getFluidState(feet).isEmpty()
|
||||
&& level.getBlockState(head).getCollisionShape(level, head).isEmpty()
|
||||
&& level.getFluidState(head).isEmpty()
|
||||
&& level.getBlockState(floor).isFaceSturdy(level, floor, Direction.UP);
|
||||
}
|
||||
|
||||
public enum LinkResult {
|
||||
SUCCESS,
|
||||
MISSING_DIMENSION,
|
||||
NO_SAFE_DESTINATION
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResultHolder;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class LinkingBookItem extends Item {
|
||||
private static final int LINK_COOLDOWN_TICKS = 40;
|
||||
|
||||
public LinkingBookItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand usedHand) {
|
||||
ItemStack stack = player.getItemInHand(usedHand);
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) {
|
||||
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide());
|
||||
}
|
||||
|
||||
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
|
||||
if (target == null) {
|
||||
if (!player.isShiftKeyDown()) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bind_hint"), true);
|
||||
return InteractionResultHolder.fail(stack);
|
||||
}
|
||||
|
||||
target = LinkTarget.from(serverPlayer);
|
||||
stack.set(ModDataComponents.LINK_TARGET.get(), target);
|
||||
level.playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bound"), true);
|
||||
return InteractionResultHolder.success(stack);
|
||||
}
|
||||
|
||||
LinkingService.LinkResult result = LinkingService.travel(serverPlayer, target);
|
||||
if (result == LinkingService.LinkResult.SUCCESS) {
|
||||
serverPlayer.getCooldowns().addCooldown(this, LINK_COOLDOWN_TICKS);
|
||||
serverPlayer.level().playSound(
|
||||
null,
|
||||
serverPlayer.blockPosition(),
|
||||
SoundEvents.PORTAL_TRAVEL,
|
||||
SoundSource.PLAYERS,
|
||||
0.35F,
|
||||
1.0F
|
||||
);
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.linked"), true);
|
||||
return InteractionResultHolder.success(stack);
|
||||
}
|
||||
|
||||
String errorKey = result == LinkingService.LinkResult.MISSING_DIMENSION
|
||||
? "message.rivencraft.linking_book.missing_dimension"
|
||||
: "message.rivencraft.linking_book.unsafe";
|
||||
player.displayClientMessage(Component.translatable(errorKey), true);
|
||||
return InteractionResultHolder.fail(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoil(ItemStack stack) {
|
||||
return stack.has(ModDataComponents.LINK_TARGET.get()) || super.isFoil(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack,
|
||||
TooltipContext context,
|
||||
List<Component> tooltipComponents,
|
||||
TooltipFlag tooltipFlag
|
||||
) {
|
||||
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
|
||||
if (target == null) {
|
||||
tooltipComponents.add(Component.translatable("item.rivencraft.linking_book.unbound")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceLocation dimension = target.location().dimension().location();
|
||||
BlockPos position = target.location().pos();
|
||||
tooltipComponents.add(Component.translatable(
|
||||
"item.rivencraft.linking_book.destination",
|
||||
dimension.toString(),
|
||||
position.getX(),
|
||||
position.getY(),
|
||||
position.getZ()
|
||||
)
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,10 @@ public final class ModCreativeTabs {
|
||||
CREATIVE_TABS.register("rivencraft", () -> CreativeModeTab.builder()
|
||||
.title(Component.translatable("itemGroup.rivencraft"))
|
||||
.icon(() -> ModItems.DESCRIPTIVE_BOOK.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> output.accept(ModItems.DESCRIPTIVE_BOOK.get()))
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
|
||||
output.accept(ModItems.LINKING_BOOK.get());
|
||||
})
|
||||
.build());
|
||||
|
||||
private ModCreativeTabs() {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
public final class ModDataComponents {
|
||||
private static final DeferredRegister<DataComponentType<?>> DATA_COMPONENTS =
|
||||
DeferredRegister.create(Registries.DATA_COMPONENT_TYPE, Rivencraft.MOD_ID);
|
||||
|
||||
public static final DeferredHolder<DataComponentType<?>, DataComponentType<LinkTarget>> LINK_TARGET =
|
||||
DATA_COMPONENTS.register("link_target", () -> DataComponentType.<LinkTarget>builder()
|
||||
.persistent(LinkTarget.CODEC)
|
||||
.networkSynchronized(LinkTarget.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build());
|
||||
|
||||
private ModDataComponents() {
|
||||
}
|
||||
|
||||
public static void register(IEventBus modEventBus) {
|
||||
DATA_COMPONENTS.register(modEventBus);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredItem;
|
||||
@@ -15,6 +16,11 @@ public final class ModItems {
|
||||
DescriptiveBookItem::new,
|
||||
new Item.Properties().stacksTo(1)
|
||||
);
|
||||
public static final DeferredItem<LinkingBookItem> LINKING_BOOK = ITEMS.registerItem(
|
||||
"linking_book",
|
||||
LinkingBookItem::new,
|
||||
new Item.Properties().stacksTo(1)
|
||||
);
|
||||
|
||||
private ModItems() {
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"item.rivencraft.descriptive_book": "Descriptive Book",
|
||||
"item.rivencraft.descriptive_book.unwritten": "Unwritten",
|
||||
"item.rivencraft.linking_book": "Linking Book",
|
||||
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
|
||||
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
|
||||
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
|
||||
"message.rivencraft.linking_book.bound": "Linking Book bound",
|
||||
"message.rivencraft.linking_book.linked": "Linked",
|
||||
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
|
||||
"message.rivencraft.linking_book.unsafe": "No safe destination was found near the link",
|
||||
"itemGroup.rivencraft": "Rivencraft"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "minecraft:item/enchanted_book"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
{
|
||||
"item": "minecraft:book"
|
||||
},
|
||||
{
|
||||
"item": "minecraft:ender_pearl"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"count": 1,
|
||||
"id": "rivencraft:linking_book"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user