Public Access
1 parent
a17083309d
commit
1460e3e4bd
17 files changed
+415
-95
No files matched your search
@@ -5,9 +5,9 @@ Rivencraft is a clean, incremental port of the ideas and gameplay of
|
||||
1.21.1 on NeoForge.
|
||||
|
||||
The bootstrap and books/linking milestones are complete. The project currently
|
||||
provides functional Linking Books and temporary vanilla-dimension Descriptive
|
||||
Book destinations with generated Return Links. Age generation, symbols,
|
||||
instability, and writing mechanics have not been ported yet.
|
||||
provides functional Linking Books and randomly seeded, persistent vanilla-like
|
||||
Ages created from page-free Descriptive Books. Symbols, instability, and the
|
||||
full writing mechanics have not been ported yet.
|
||||
|
||||
## Linking Book
|
||||
|
||||
@@ -33,13 +33,16 @@ independently validates every link.
|
||||
|
||||
## Descriptive Book
|
||||
|
||||
Until generated Ages arrive in Milestone 3, written Descriptive Books resolve
|
||||
deterministically to the Overworld, Nether, or End according to their Age UUID.
|
||||
The Rivencraft creative tab includes one sample book for each. Linking creates
|
||||
a bound Return Link to the exact source position and orientation on an Age's
|
||||
first successful visit. Descriptive
|
||||
Book destinations additionally require solid footing beneath the player. If
|
||||
their recorded arrival is unsafe, they search nearby terrain for a footed spot.
|
||||
Open a page-free Descriptive Book, name its Age, and press Create. Rivencraft
|
||||
assigns the book a permanent Age UUID, creates a dedicated vanilla-like runtime
|
||||
dimension, and prepares a five-by-five cobblestone arrival platform at the
|
||||
highest suitable nearby terrain. If no sturdy surface is available, the
|
||||
platform is generated at Y=63. Reopen the book and click its preview to travel.
|
||||
Copies of the written book continue to resolve to the same persistent Age
|
||||
across server restarts.
|
||||
|
||||
Linking creates a bound Return Link to the exact source position and orientation
|
||||
according to the configured Return Link policy.
|
||||
|
||||
Server owners can set `descriptive_books.return_link_mode` in Rivencraft's
|
||||
server config to `ON_CREATION` (default), `ALWAYS`, or `DISABLED`.
|
||||
@@ -50,6 +53,7 @@ server config to `ON_CREATION` (default), `ALWAYS`, or `DISABLED`.
|
||||
- Minecraft 1.21.1
|
||||
- NeoForge 21.1.251 or newer in the 21.1 line
|
||||
- Ponder 1.0.82 for Minecraft 1.21.1 (client)
|
||||
- Dynamic Dimensions AR Edition 0.9.1 and Bad Packets 0.8.2 (client and server)
|
||||
|
||||
## Build and run
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ sourceSets.main.resources {
|
||||
|
||||
repositories {
|
||||
maven { url = 'https://maven.createmod.net' }
|
||||
maven {
|
||||
url = 'https://www.cursemaven.com'
|
||||
content { includeGroup 'curse.maven' }
|
||||
}
|
||||
maven { url = 'https://maven.bai.lol' }
|
||||
}
|
||||
|
||||
base {
|
||||
@@ -117,6 +122,8 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
||||
implementation "curse.maven:dynamicdimension-ar-edition-1469680:${dynamic_dimensions_file_id}"
|
||||
implementation "lol.bai:badpackets:neo-${badpackets_version}"
|
||||
// Example optional mod dependency with JEI
|
||||
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
|
||||
// compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
|
||||
|
||||
@@ -190,6 +190,13 @@ generation profile, generation-version marker, canonical name, and creation
|
||||
time. The temporary vanilla-dimension adapter remains in place until runtime
|
||||
Age dimensions are introduced by a later release slice.
|
||||
|
||||
Version 0.2.10.0 makes page-free Descriptive Books playable. Naming an
|
||||
unwritten book creates a dedicated, persistent vanilla-like Age through the
|
||||
Dynamic Dimensions API. Each Age reloads with its server, retains its terrain,
|
||||
and receives a five-by-five cobblestone platform with cleared arrival headroom.
|
||||
Non-Nether Ages select the highest sturdy surface near the origin and fall back
|
||||
to a platform at Y=63 when no suitable natural footing exists.
|
||||
|
||||
The preview code is separated behind a renderer interface and uses an isolated
|
||||
Ponder level populated from server-authoritative destination snapshots.
|
||||
|
||||
|
||||
+3
-1
@@ -20,6 +20,8 @@ minecraft_version_range=[1.21.1]
|
||||
# The Neo version must agree with the Minecraft version to get a valid artifact
|
||||
neo_version=21.1.251
|
||||
ponder_version=1.0.82
|
||||
dynamic_dimensions_file_id=7707789
|
||||
badpackets_version=0.8.2
|
||||
# The loader version range can only use the major version of FML as bounds
|
||||
loader_version_range=[1,)
|
||||
|
||||
@@ -33,7 +35,7 @@ mod_name=Rivencraft
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=LGPL-3.0-or-later
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.2.9.0
|
||||
mod_version=0.2.10.0
|
||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||
# This should match the base package used for the mod sources.
|
||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||
|
||||
@@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
|
||||
import net.dnorberg.rivencraft.network.ModNetworking;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.ModContainer;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
@@ -22,6 +23,7 @@ public final class Rivencraft {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
public Rivencraft(IEventBus modEventBus, ModContainer modContainer) {
|
||||
AgeDimensionService.register();
|
||||
modContainer.registerConfig(ModConfig.Type.SERVER, RivencraftConfig.SPEC);
|
||||
modEventBus.addListener(ModNetworking::register);
|
||||
ModDataComponents.register(modEventBus);
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.book;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** Immutable, server-owned generation definition for one Age. */
|
||||
@@ -12,7 +13,9 @@ public record AgeDefinition(
|
||||
long seed,
|
||||
ResourceLocation generationProfile,
|
||||
int generationVersion,
|
||||
long createdGameTime
|
||||
long createdGameTime,
|
||||
ResourceLocation dimensionId,
|
||||
BlockPos spawn
|
||||
) {
|
||||
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "random_vanilla"
|
||||
@@ -23,5 +26,15 @@ public record AgeDefinition(
|
||||
Objects.requireNonNull(id, "id");
|
||||
Objects.requireNonNull(name, "name");
|
||||
Objects.requireNonNull(generationProfile, "generationProfile");
|
||||
Objects.requireNonNull(dimensionId, "dimensionId");
|
||||
}
|
||||
|
||||
public static ResourceLocation dimensionId(UUID ageId) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + ageId);
|
||||
}
|
||||
|
||||
public AgeDefinition withSpawn(BlockPos spawn) {
|
||||
return new AgeDefinition(id, name, seed, generationProfile, generationVersion,
|
||||
createdGameTime, dimensionId, spawn.immutable());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
|
||||
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.GlobalPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.valueproviders.UniformInt;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** Creates, reloads, and resolves Rivencraft's runtime Age dimensions. */
|
||||
public final class AgeDimensionService {
|
||||
private static final int PLATFORM_RADIUS = 2;
|
||||
private static final int SURFACE_SEARCH_RADIUS = 16;
|
||||
private static final int FALLBACK_PLATFORM_Y = 63;
|
||||
|
||||
private AgeDimensionService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
DynamicDimensionLoadCallback.register((server, loader) -> {
|
||||
ServerLevel overworld = server.overworld();
|
||||
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
|
||||
loader.loadDynamicDimension(
|
||||
age.dimensionId(),
|
||||
overworld.getChunkSource().getGenerator(),
|
||||
createDimensionType()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference) {
|
||||
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference);
|
||||
ServerLevel level = ensureLoaded(player.getServer(), definition);
|
||||
if (level == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
BlockPos spawn = definition.spawn();
|
||||
if (spawn == null) {
|
||||
spawn = createSafeSpawn(level);
|
||||
definition = AgeRegistrySavedData.get(level).setSpawn(definition.id(), spawn);
|
||||
}
|
||||
return Optional.of(new LinkTarget(
|
||||
GlobalPos.of(dimensionKey(definition), spawn),
|
||||
level.getSharedSpawnAngle(),
|
||||
0.0F
|
||||
));
|
||||
}
|
||||
|
||||
public static ServerLevel ensureLoaded(MinecraftServer server, AgeDefinition definition) {
|
||||
ResourceKey<Level> key = dimensionKey(definition);
|
||||
ServerLevel existing = server.getLevel(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
ServerLevel overworld = server.overworld();
|
||||
ServerLevel loaded = DynamicDimensionRegistry.from(server).loadDynamicDimension(
|
||||
definition.dimensionId(),
|
||||
overworld.getChunkSource().getGenerator(),
|
||||
createDimensionType()
|
||||
);
|
||||
if (loaded == null) {
|
||||
Rivencraft.LOGGER.error("Failed to load Age {} as {}", definition.id(), definition.dimensionId());
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public static ResourceKey<Level> dimensionKey(AgeDefinition definition) {
|
||||
return ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, definition.dimensionId());
|
||||
}
|
||||
|
||||
private static BlockPos createSafeSpawn(ServerLevel level) {
|
||||
BlockPos floorCenter = isNetherStyle(level)
|
||||
? netherSpawnFloor(level)
|
||||
: highestFootedSurface(level).orElseGet(() -> fallbackSpawnFloor(level));
|
||||
|
||||
for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) {
|
||||
for (int z = -PLATFORM_RADIUS; z <= PLATFORM_RADIUS; z++) {
|
||||
BlockPos floor = floorCenter.offset(x, 0, z);
|
||||
level.setBlock(floor, Blocks.COBBLESTONE.defaultBlockState(), 2);
|
||||
for (int y = 1; y <= 4; y++) {
|
||||
level.setBlock(floor.above(y), Blocks.AIR.defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BlockPos spawn = floorCenter.above();
|
||||
level.setDefaultSpawnPos(spawn, 0.0F);
|
||||
return spawn;
|
||||
}
|
||||
|
||||
private static boolean isNetherStyle(ServerLevel level) {
|
||||
DimensionType type = level.dimensionType();
|
||||
return type.ultraWarm() || type.effectsLocation().equals(BuiltinDimensionTypes.NETHER_EFFECTS);
|
||||
}
|
||||
|
||||
private static BlockPos netherSpawnFloor(ServerLevel level) {
|
||||
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, 0, 0);
|
||||
int floorY = Math.clamp(surfaceY - 1, level.getMinBuildHeight(), level.getMaxBuildHeight() - 5);
|
||||
return new BlockPos(0, floorY, 0);
|
||||
}
|
||||
|
||||
/** Selects the highest nearby terrain column that can naturally support a player. */
|
||||
private static Optional<BlockPos> highestFootedSurface(ServerLevel level) {
|
||||
BlockPos best = null;
|
||||
int maximumFloorY = level.getMaxBuildHeight() - 5;
|
||||
|
||||
for (int x = -SURFACE_SEARCH_RADIUS; x <= SURFACE_SEARCH_RADIUS; x++) {
|
||||
for (int z = -SURFACE_SEARCH_RADIUS; z <= SURFACE_SEARCH_RADIUS; z++) {
|
||||
int floorY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||
if (floorY < level.getMinBuildHeight() || floorY > maximumFloorY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockPos candidate = new BlockPos(x, floorY, z);
|
||||
if (!level.getFluidState(candidate).isEmpty()
|
||||
|| !level.getBlockState(candidate).isFaceSturdy(level, candidate, net.minecraft.core.Direction.UP)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (best == null || candidate.getY() > best.getY()
|
||||
|| (candidate.getY() == best.getY()
|
||||
&& candidate.distSqr(BlockPos.ZERO) < best.distSqr(BlockPos.ZERO))) {
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.ofNullable(best);
|
||||
}
|
||||
|
||||
private static BlockPos fallbackSpawnFloor(ServerLevel level) {
|
||||
int floorY = Math.clamp(FALLBACK_PLATFORM_Y,
|
||||
level.getMinBuildHeight(), level.getMaxBuildHeight() - 5);
|
||||
return new BlockPos(0, floorY, 0);
|
||||
}
|
||||
|
||||
private static DimensionType createDimensionType() {
|
||||
return new DimensionType(
|
||||
OptionalLong.empty(),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
1.0D,
|
||||
false,
|
||||
false,
|
||||
-64,
|
||||
384,
|
||||
384,
|
||||
BlockTags.INFINIBURN_OVERWORLD,
|
||||
BuiltinDimensionTypes.OVERWORLD_EFFECTS,
|
||||
0.0F,
|
||||
new DimensionType.MonsterSettings(false, true, UniformInt.of(0, 7), 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
@@ -22,6 +24,10 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
private static final String PROFILE_TAG = "GenerationProfile";
|
||||
private static final String VERSION_TAG = "GenerationVersion";
|
||||
private static final String CREATED_TIME_TAG = "CreatedGameTime";
|
||||
private static final String DIMENSION_TAG = "Dimension";
|
||||
private static final String SPAWN_X_TAG = "SpawnX";
|
||||
private static final String SPAWN_Y_TAG = "SpawnY";
|
||||
private static final String SPAWN_Z_TAG = "SpawnZ";
|
||||
private static final Factory<AgeRegistrySavedData> FACTORY = new Factory<>(
|
||||
AgeRegistrySavedData::new,
|
||||
AgeRegistrySavedData::load,
|
||||
@@ -44,6 +50,21 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
return Optional.ofNullable(ages.get(ageId));
|
||||
}
|
||||
|
||||
public List<AgeDefinition> definitions() {
|
||||
return List.copyOf(ages.values());
|
||||
}
|
||||
|
||||
public AgeDefinition setSpawn(UUID ageId, BlockPos spawn) {
|
||||
AgeDefinition existing = ages.get(ageId);
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("Unknown Age " + ageId);
|
||||
}
|
||||
AgeDefinition updated = existing.withSpawn(spawn);
|
||||
ages.put(ageId, updated);
|
||||
setDirty();
|
||||
return updated;
|
||||
}
|
||||
|
||||
private AgeDefinition getOrCreate(AgeReference reference, long worldSeed, long createdGameTime) {
|
||||
AgeDefinition existing = ages.get(reference.id());
|
||||
if (existing != null) {
|
||||
@@ -56,7 +77,9 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
ageSeed(worldSeed, reference.id()),
|
||||
AgeDefinition.RANDOM_VANILLA_PROFILE,
|
||||
AgeDefinition.CURRENT_GENERATION_VERSION,
|
||||
createdGameTime
|
||||
createdGameTime,
|
||||
AgeDefinition.dimensionId(reference.id()),
|
||||
null
|
||||
);
|
||||
ages.put(created.id(), created);
|
||||
setDirty();
|
||||
@@ -76,13 +99,22 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
profile = AgeDefinition.RANDOM_VANILLA_PROFILE;
|
||||
}
|
||||
UUID id = entry.getUUID(ID_TAG);
|
||||
ResourceLocation dimensionId = ResourceLocation.tryParse(entry.getString(DIMENSION_TAG));
|
||||
if (dimensionId == null) {
|
||||
dimensionId = AgeDefinition.dimensionId(id);
|
||||
}
|
||||
BlockPos spawn = entry.contains(SPAWN_Y_TAG)
|
||||
? new BlockPos(entry.getInt(SPAWN_X_TAG), entry.getInt(SPAWN_Y_TAG), entry.getInt(SPAWN_Z_TAG))
|
||||
: null;
|
||||
data.ages.put(id, new AgeDefinition(
|
||||
id,
|
||||
normalizedName(entry.getString(NAME_TAG)),
|
||||
entry.getLong(SEED_TAG),
|
||||
profile,
|
||||
entry.contains(VERSION_TAG) ? entry.getInt(VERSION_TAG) : 1,
|
||||
entry.getLong(CREATED_TIME_TAG)
|
||||
entry.getLong(CREATED_TIME_TAG),
|
||||
dimensionId,
|
||||
spawn
|
||||
));
|
||||
}
|
||||
return data;
|
||||
@@ -101,6 +133,12 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
entry.putString(PROFILE_TAG, age.generationProfile().toString());
|
||||
entry.putInt(VERSION_TAG, age.generationVersion());
|
||||
entry.putLong(CREATED_TIME_TAG, age.createdGameTime());
|
||||
entry.putString(DIMENSION_TAG, age.dimensionId().toString());
|
||||
if (age.spawn() != null) {
|
||||
entry.putInt(SPAWN_X_TAG, age.spawn().getX());
|
||||
entry.putInt(SPAWN_Y_TAG, age.spawn().getY());
|
||||
entry.putInt(SPAWN_Z_TAG, age.spawn().getZ());
|
||||
}
|
||||
serializedAges.add(entry);
|
||||
});
|
||||
tag.put(AGES_TAG, serializedAges);
|
||||
@@ -113,15 +151,8 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
}
|
||||
|
||||
private static long ageSeed(long worldSeed, UUID ageId) {
|
||||
long value = worldSeed ^ ageId.getMostSignificantBits();
|
||||
value = mix64(value + ageId.getLeastSignificantBits());
|
||||
return mix64(value ^ 0x524956454E435241L);
|
||||
}
|
||||
|
||||
/** SplitMix64 finalizer, used only to derive a stable seed from world and Age identities. */
|
||||
private static long mix64(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL;
|
||||
return value ^ (value >>> 31);
|
||||
// Dynamic Dimensions AR derives each level seed from the primary world
|
||||
// seed and the dimension key. Persist the same value as Age metadata.
|
||||
return worldSeed + AgeDefinition.dimensionId(ageId).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.GlobalPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Temporary Milestone 2 destination resolver.
|
||||
*
|
||||
* <p>Until Ages own generated dimensions, an Age UUID deterministically selects
|
||||
* one of the three vanilla dimensions. Milestone 3 will replace this with the
|
||||
* persistent Age-to-dimension mapping without changing book identities.</p>
|
||||
*/
|
||||
public final class VanillaAgeResolver {
|
||||
private static final List<ResourceKey<Level>> VANILLA_DIMENSIONS = List.of(
|
||||
Level.OVERWORLD,
|
||||
Level.NETHER,
|
||||
Level.END
|
||||
);
|
||||
private static final List<AgeReference> DEMO_AGES = List.of(
|
||||
new AgeReference(new UUID(0L, 0L), "Overworld Age"),
|
||||
new AgeReference(new UUID(0L, 1L), "Nether Age"),
|
||||
new AgeReference(new UUID(0L, 2L), "End Age")
|
||||
);
|
||||
|
||||
private VanillaAgeResolver() {
|
||||
}
|
||||
|
||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference age) {
|
||||
// Milestone 3 owns Age identity and generation data even while this
|
||||
// temporary adapter continues to provide a vanilla travel destination.
|
||||
AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), age);
|
||||
ResourceKey<Level> dimension = VANILLA_DIMENSIONS.get(Math.floorMod(age.id().hashCode(), VANILLA_DIMENSIONS.size()));
|
||||
ServerLevel level = player.getServer().getLevel(dimension);
|
||||
if (level == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new LinkTarget(
|
||||
GlobalPos.of(dimension, level.getSharedSpawnPos()),
|
||||
level.getSharedSpawnAngle(),
|
||||
0.0F
|
||||
));
|
||||
}
|
||||
|
||||
/** Written sample books exposed in the creative tab until the writing system exists. */
|
||||
public static List<AgeReference> demoAges() {
|
||||
return DEMO_AGES;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package net.dnorberg.rivencraft.client.screen;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.network.UseDescriptiveBookPayload;
|
||||
import net.dnorberg.rivencraft.network.BindDescriptiveBookPayload;
|
||||
import net.dnorberg.rivencraft.network.ModNetworking;
|
||||
import net.dnorberg.rivencraft.network.RequestLinkPreviewPayload;
|
||||
import net.dnorberg.rivencraft.network.LinkPreviewPayload;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
@@ -10,6 +12,8 @@ import net.dnorberg.rivencraft.client.preview.PonderDestinationPreviewRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
@@ -35,6 +39,7 @@ public final class DescriptiveBookScreen extends Screen {
|
||||
private final InteractionHand hand;
|
||||
private final BlockPos bookStandPos;
|
||||
private DestinationPreviewRenderer preview;
|
||||
private EditBox titleInput;
|
||||
|
||||
public DescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
@@ -52,7 +57,23 @@ public final class DescriptiveBookScreen extends Screen {
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (age != null) {
|
||||
int left = (width - BOOK_WIDTH) / 2;
|
||||
int top = (height - BOOK_HEIGHT) / 2;
|
||||
if (age == null) {
|
||||
int rightPage = left + 177;
|
||||
titleInput = new EditBox(font, rightPage, top + 69, 124, 20,
|
||||
Component.translatable("screen.rivencraft.age.title"));
|
||||
titleInput.setMaxLength(ModNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setHint(Component.translatable("screen.rivencraft.age.title_hint"));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.age.create"), button -> createAge())
|
||||
.bounds(rightPage, top + 100, 59, 20)
|
||||
.build());
|
||||
addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose())
|
||||
.bounds(rightPage + 65, top + 100, 59, 20)
|
||||
.build());
|
||||
} else {
|
||||
preview = new PonderDestinationPreviewRenderer();
|
||||
PacketDistributor.sendToServer(new RequestLinkPreviewPayload(
|
||||
true,
|
||||
@@ -83,7 +104,7 @@ public final class DescriptiveBookScreen extends Screen {
|
||||
TEXTURE_WIDTH, TEXTURE_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
|
||||
if (age == null) {
|
||||
drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.book.unwritten"),
|
||||
drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.age.name_prompt"),
|
||||
left + 177, top + 45, 124);
|
||||
} else {
|
||||
preview.render(graphics, left + LINK_PANEL_X, top + LINK_PANEL_Y, LINK_PANEL_WIDTH, LINK_PANEL_HEIGHT, partialTick);
|
||||
@@ -129,6 +150,20 @@ public final class DescriptiveBookScreen extends Screen {
|
||||
onClose();
|
||||
}
|
||||
|
||||
private void createAge() {
|
||||
String name = titleInput.getValue().strip();
|
||||
if (name.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
PacketDistributor.sendToServer(new BindDescriptiveBookPayload(
|
||||
bookStandPos != null,
|
||||
bookStandPos == null && hand == InteractionHand.OFF_HAND,
|
||||
bookStandPos == null ? BlockPos.ZERO : bookStandPos,
|
||||
name
|
||||
));
|
||||
onClose();
|
||||
}
|
||||
|
||||
private void drawWrappedWithoutShadow(GuiGraphics graphics, Component text, int x, int y, int maxWidth) {
|
||||
for (FormattedCharSequence line : font.split(text, maxWidth)) {
|
||||
graphics.drawString(font, line, x, y, BOOK_TEXT_COLOR, false);
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.gametest;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
@@ -102,6 +103,25 @@ public final class LinkingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty", timeoutTicks = 100)
|
||||
public static void descriptiveBookAgeCreatesRuntimeDimensionAndSafeSpawn(GameTestHelper helper) {
|
||||
ServerPlayer player = helper.makeMockServerPlayerInLevel();
|
||||
AgeReference reference = new AgeReference(UUID.randomUUID(), "Generated Test Age");
|
||||
LinkTarget target = AgeDimensionService.resolve(player, reference).orElse(null);
|
||||
|
||||
helper.assertTrue(target != null, "A page-free Descriptive Book must create a runtime Age destination");
|
||||
helper.runAfterDelay(1, () -> {
|
||||
var ageLevel = player.getServer().getLevel(target.location().dimension());
|
||||
helper.assertTrue(ageLevel != null, "The generated Age must be registered with the server");
|
||||
BlockPos spawn = target.location().pos();
|
||||
helper.assertTrue(ageLevel.getBlockState(spawn.below()).is(Blocks.COBBLESTONE),
|
||||
"The generated Age must have a cobblestone arrival platform");
|
||||
helper.assertTrue(ageLevel.getBlockState(spawn).isAir() && ageLevel.getBlockState(spawn.above()).isAir(),
|
||||
"The generated Age arrival must have clear headroom");
|
||||
helper.succeed();
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void travelUsesLeafyDestinationWithoutFloor(GameTestHelper helper) {
|
||||
helper.setBlock(ORIGIN.below(), Blocks.AIR);
|
||||
|
||||
@@ -5,7 +5,7 @@ import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeVisitSavedData;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.book.VanillaAgeResolver;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
@@ -45,7 +45,7 @@ public final class DescriptiveBookItem extends Item {
|
||||
}
|
||||
|
||||
LinkTarget returnTarget = LinkTarget.from(player);
|
||||
LinkTarget ageTarget = VanillaAgeResolver.resolve(player, age).orElse(null);
|
||||
LinkTarget ageTarget = AgeDimensionService.resolve(player, age).orElse(null);
|
||||
if (ageTarget == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** Requests creation of a randomly generated Age for an unwritten Descriptive Book. */
|
||||
public record BindDescriptiveBookPayload(boolean fromStand, boolean offhand, BlockPos standPos, String title)
|
||||
implements CustomPacketPayload {
|
||||
public static final Type<BindDescriptiveBookPayload> TYPE = new Type<>(
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "bind_descriptive_book")
|
||||
);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, BindDescriptiveBookPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, BindDescriptiveBookPayload::fromStand,
|
||||
ByteBufCodecs.BOOL, BindDescriptiveBookPayload::offhand,
|
||||
BlockPos.STREAM_CODEC, BindDescriptiveBookPayload::standPos,
|
||||
ByteBufCodecs.STRING_UTF8, BindDescriptiveBookPayload::title,
|
||||
BindDescriptiveBookPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.VanillaAgeResolver;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
@@ -26,6 +26,7 @@ import net.neoforged.neoforge.network.registration.PayloadRegistrar;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ModNetworking {
|
||||
public static final int MAX_BOOK_TITLE_LENGTH = 32;
|
||||
@@ -34,8 +35,10 @@ public final class ModNetworking {
|
||||
}
|
||||
|
||||
public static void register(RegisterPayloadHandlersEvent event) {
|
||||
PayloadRegistrar registrar = event.registrar("5");
|
||||
PayloadRegistrar registrar = event.registrar("6");
|
||||
registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook);
|
||||
registrar.playToServer(BindDescriptiveBookPayload.TYPE, BindDescriptiveBookPayload.STREAM_CODEC,
|
||||
ModNetworking::bindDescriptiveBook);
|
||||
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook);
|
||||
registrar.playToServer(UseDescriptiveBookPayload.TYPE, UseDescriptiveBookPayload.STREAM_CODEC,
|
||||
ModNetworking::useDescriptiveBook);
|
||||
@@ -46,6 +49,33 @@ public final class ModNetworking {
|
||||
ClientLinkPreviewHandler::handle);
|
||||
}
|
||||
|
||||
private static void bindDescriptiveBook(BindDescriptiveBookPayload payload, IPayloadContext context) {
|
||||
if (!(context.player() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BookAccess access = descriptiveBookAccess(player, payload.fromStand(), payload.offhand(), payload.standPos());
|
||||
if (access == null || access.stack().has(ModDataComponents.AGE_REFERENCE.get())) {
|
||||
return;
|
||||
}
|
||||
String title = normalizeTitle(payload.title());
|
||||
if (title.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
AgeReference reference = new AgeReference(UUID.randomUUID(), title);
|
||||
if (AgeDimensionService.resolve(player, reference).isEmpty()) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
access.stack().set(ModDataComponents.AGE_REFERENCE.get(), reference);
|
||||
access.stack().set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
access.sync().run();
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.created", title), true);
|
||||
}
|
||||
|
||||
private static void bindBook(BindLinkingBookPayload payload, IPayloadContext context) {
|
||||
if (!(context.player() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
@@ -131,7 +161,7 @@ public final class ModNetworking {
|
||||
LinkTarget target;
|
||||
if (payload.descriptiveBook() && stack.is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
target = age == null ? null : VanillaAgeResolver.resolve(player, age).orElse(null);
|
||||
target = age == null ? null : AgeDimensionService.resolve(player, age).orElse(null);
|
||||
} else if (!payload.descriptiveBook() && stack.is(ModItems.LINKING_BOOK.get())) {
|
||||
target = stack.get(ModDataComponents.LINK_TARGET.get());
|
||||
} else {
|
||||
@@ -186,4 +216,25 @@ public final class ModNetworking {
|
||||
? normalized
|
||||
: normalized.substring(0, MAX_BOOK_TITLE_LENGTH).stripTrailing();
|
||||
}
|
||||
|
||||
private static BookAccess descriptiveBookAccess(
|
||||
ServerPlayer player,
|
||||
boolean fromStand,
|
||||
boolean offhand,
|
||||
BlockPos standPos
|
||||
) {
|
||||
if (fromStand) {
|
||||
if (player.distanceToSqr(Vec3.atCenterOf(standPos)) > 64.0D
|
||||
|| !(player.level().getBlockEntity(standPos) instanceof BookStandBlockEntity stand)
|
||||
|| !stand.getBook().is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
return null;
|
||||
}
|
||||
return new BookAccess(stand.getBook(), () -> stand.setBook(stand.getBook()));
|
||||
}
|
||||
ItemStack stack = player.getItemInHand(hand(offhand));
|
||||
return stack.is(ModItems.DESCRIPTIVE_BOOK.get()) ? new BookAccess(stack, () -> { }) : null;
|
||||
}
|
||||
|
||||
private record BookAccess(ItemStack stack, Runnable sync) {
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.VanillaAgeResolver;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
@@ -21,12 +18,6 @@ public final class ModCreativeTabs {
|
||||
.icon(() -> ModItems.DESCRIPTIVE_BOOK.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
|
||||
VanillaAgeResolver.demoAges().forEach(age -> {
|
||||
ItemStack book = new ItemStack(ModItems.DESCRIPTIVE_BOOK.get());
|
||||
book.set(ModDataComponents.AGE_REFERENCE.get(), age);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(age.name()));
|
||||
output.accept(book);
|
||||
});
|
||||
output.accept(ModItems.LINKING_BOOK.get());
|
||||
})
|
||||
.build());
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
|
||||
"message.rivencraft.linking_book.unsafe": "No safe destination was found near the link",
|
||||
"message.rivencraft.descriptive_book.unwritten": "This Descriptive Book is unwritten",
|
||||
"message.rivencraft.descriptive_book.created": "Created Age: %s",
|
||||
"message.rivencraft.descriptive_book.unavailable": "That Age is unavailable",
|
||||
"message.rivencraft.descriptive_book.unsafe": "No safe destination was found in that Age",
|
||||
"message.rivencraft.descriptive_book.linked_first": "Linked to %s — a Return Link was created",
|
||||
@@ -23,6 +24,10 @@
|
||||
"screen.rivencraft.book.bind": "Bind",
|
||||
"screen.rivencraft.book.link": "Link",
|
||||
"screen.rivencraft.book.unwritten": "This Descriptive Book is unwritten",
|
||||
"screen.rivencraft.age.title": "Age name",
|
||||
"screen.rivencraft.age.title_hint": "Name this Age",
|
||||
"screen.rivencraft.age.name_prompt": "Name this Age before writing the Descriptive Book",
|
||||
"screen.rivencraft.age.create": "Create",
|
||||
"screen.rivencraft.book.preview_pending": "Loading destination...",
|
||||
"screen.rivencraft.book.click_to_link": "Click the panel to link",
|
||||
"itemGroup.rivencraft": "Rivencraft"
|
||||
|
||||
@@ -93,6 +93,13 @@ A spiritual successor and modern port of Mystcraft for Minecraft 1.21.1 on NeoFo
|
||||
ordering="AFTER"
|
||||
side="CLIENT"
|
||||
|
||||
[[dependencies.${mod_id}]]
|
||||
modId="dynamicdimensions"
|
||||
type="required"
|
||||
versionRange="[0.9.1,1.0.0)"
|
||||
ordering="AFTER"
|
||||
side="BOTH"
|
||||
|
||||
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
|
||||
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
|
||||
# stop your mod loading on the server for example.
|
||||
|
||||
Reference in new issue
Block a user