Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f1d458897 | ||
|
|
0c89cfc464 |
No files matched your search
@@ -28,6 +28,7 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
|
||||
| `assets/rivencraft/textures/block/crystal_block.png`, `budding_crystal.png` | `assets/mystcraft/textures/blocks/crystal.png` | Legacy 0.13.7.06 local extraction | Exact 16×16 blue tile reused for both solid crystal blocks |
|
||||
| `assets/rivencraft/textures/block/{small,medium,large}_crystal_bud.png`, `crystal_cluster.png` | Minecraft 1.21.1 `textures/block/{small,medium,large}_amethyst_bud.png`, `amethyst_cluster.png` | Minecraft 1.21.1 | Original 16×16 alpha masks and silhouettes preserved; hue shifted toward Legacy blue, with brighter saturation; exported as 8-bit RGBA (not indexed PNG) |
|
||||
| `assets/rivencraft/textures/item/crystal_shard.png` | Original sprite generated with OpenAI ImageGen from the Legacy crystal palette | 2026-10-05 | Cropped and nearest-neighbor scaled to 16×16 |
|
||||
| `assets/rivencraft/sounds/linking/link-fissure.ogg` | `assets/mystcraft/sounds/linking/link-fissure.ogg` | Legacy 0.13.7.06 local extraction | Namespace/path rename only; plays when a Star Fissure returns an entity home |
|
||||
|
||||
Mystcraft Legacy identifies XCompWiz as its project developer and includes the
|
||||
full LGPL v3 text in its root `LICENSE` file. Rivencraft's root `LICENSE`
|
||||
|
||||
@@ -18,7 +18,7 @@ Legacy does **not** reject a book just because it omits terrain, biome, weather,
|
||||
| `WeatherNorm`, `Fast`, `Slow`, `Off`, `On`, `Cloudy`, `Rain`, `Snow`, `Storm` | `AgeTraits.Weather` has matching names except Legacy `On` maps to `ALWAYS` | Compile weather semantics separately from biome selection. `Snow` must remain deferred until optional season integration; do not silently substitute another weather mode. |
|
||||
| Suns, moons, starfields; angle, phase, period, color/gradient/sunset modifiers | One `celestialPeriod`, `celestialAngle`, `celestialPhase` on `AgeTraits` | Compile only the representable single-cycle subset first. Multiple celestial bodies, independent periods, custom stars, gradients and sunsets need a richer definition and renderer, not flattening into one value. |
|
||||
| Sky/fog/cloud/grass/foliage/water colors, natural variants, horizon, rainbow | Existing sky/dimension visuals and preview | Require an explicit modern client/biome-color design; metadata-only symbol pages must not pretend to change rendering. |
|
||||
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, authored Floating Islands/Tendrils/Skylands/Spheres/Spikes/Obelisks/Crystal Formations and Dense Ores, and page-gated vanilla Dungeons/Mineshafts/Nether Fortresses/Strongholds/Villages | The five Legacy vanilla structure symbols gate Minecraft 1.21.1 generation. Dungeons use monster-room placement; the others use vanilla structure sets. Obelisks use a bespoke 4×4 plinth and 2×12×2 shaft with safe modern placement. Crystal Formations retain Legacy's blue tile and one-in-fifteen attempt rate but use new budding, shard-producing mounds. Fissures and exact 1.12 structure geometry remain separate work. |
|
||||
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, authored Floating Islands/Tendrils/Skylands/Spheres/Spikes/Obelisks/Crystal Formations/Star Fissures and Dense Ores, and page-gated vanilla Dungeons/Mineshafts/Nether Fortresses/Strongholds/Villages | The five Legacy vanilla structure symbols gate Minecraft 1.21.1 generation. Dungeons use monster-room placement; the others use vanilla structure sets. Obelisks use a bespoke 4×4 plinth and 2×12×2 shaft with safe modern placement. Crystal Formations retain Legacy's blue tile and one-in-fifteen attempt rate but use new budding, shard-producing mounds. Star Fissures reproduce Legacy's spawn-chunk footprint, open-to-sky columns, home link, and sound. Their renderer uses the modern End Portal pipeline in place of Legacy's retired fixed-function OpenGL calls. Exact 1.12 geometry for other structures remains separate work. |
|
||||
| `EnvAccel`, explosions, lightning, meteors, scorching; PvP and dummy suppression pages | No equivalent Age-definition effect | Defer environmental hazards/instability effects to milestone 6, PvP rule to a separately scoped server policy; retain authored IDs without fake behavior. |
|
||||
|
||||
The Legacy symbol list and page names come from [ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L129-L240) and [SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java#L19-L137). Rivencraft's [catalog](../src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java) currently holds display metadata only; its built-in set is just standard terrain, native biomes, normal weather, normal lighting, normal time, and caves. [OrderedWriting](../src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java) preserves page order, but no compiler yet connects it to [AgeTraits](../src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java) or [Age profiles](AGE_PROFILES.md).
|
||||
|
||||
@@ -20,4 +20,6 @@ Material pages are generated for registered full-collision, non-fluid blocks and
|
||||
|
||||
Crystal Formations are an authored, material-free feature on exposed solid terrain, including Flat terrain; Cave and Void reject the page. Like Legacy, one in fifteen eligible chunks attempts a formation, with the site fixed by the Age seed and chunk coordinates. The modern formation is a compact mound with budding hearts instead of Legacy's long crystal lines. Budding blocks grow through small, medium, and large buds into clusters using vanilla amethyst timing and sounds. Non-silk cluster harvest yields Crystal Shards; four shards craft one Crystal Block. These blocks and shards use 16×16 textures: Legacy's crystal tile for solids and hue-shifted vanilla amethyst sprites for buds and clusters. A page-free Age can also randomly select Crystal Formations; an authored page always retains its exact choice in the saved plan.
|
||||
|
||||
Star Fissures are a material-free feature on every terrain profile, including Cave and Void. The fissure is generated once beside the first resolved spawn, using Legacy's spawn-chunk offset and 10–17 paired, irregular X-span rows running along +Z. Legacy's y=0 portal floor maps to the modern dimension's minimum build height, replacing lower bedrock if present. The rift opens to the sky on ordinary terrain; in Cave terrain it opens into the cavern but stops below the upper bedrock roof. Water in the carved columns is cleared before the portal is placed. Its non-solid portal links to the Overworld spawn with the Legacy fissure sound; the animated starfield is drawn with Minecraft's current End Portal renderer, which uses the same end-sky/end-portal art as Legacy's custom renderer. Random Ages sample Legacy's default small-feature grammar for fissure selection: the stop/one/repeat branches have rank-derived weights 1:1:3, and the fissure has weight 1 of 9 among its small-feature rules. The remainder of random feature selection still follows Rivencraft's current pipeline, rather than running Legacy's entire Age grammar.
|
||||
|
||||
The Rivencraft symbol-page creative tabs read a generated index of bundled symbol definitions and group Pages by each definition's category, except biome-controller roles, which appear in a separate Biome Generation tab rather than the Biome tab. Adding another valid JSON symbol under `src/main/resources/data/rivencraft/rivencraft/symbols/` includes its page in the matching category tab on the next build; a new bundled category receives its own tab. Registry-derived Block and Fluid Pages have separate tabs and both appear in optional JEI. Link Pages have a separate tab. Blank Pages appear in neither the creative tabs nor JEI's ingredient list. The server catalog still decides whether a page is valid for writing. External server data-pack symbols are not included in this bundled index.
|
||||
+1
-1
@@ -36,7 +36,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.4.18.2
|
||||
mod_version=0.4.19.2
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=B6nTjh4b
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.registry.ModSounds;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Portal;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.Fluid;
|
||||
import net.minecraft.world.level.portal.DimensionTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.BooleanOp;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** A starry, non-obtainable portal at the base of a generated fissure. */
|
||||
public final class StarFissureBlock extends BaseEntityBlock implements Portal {
|
||||
public static final MapCodec<StarFissureBlock> CODEC = simpleCodec(StarFissureBlock::new);
|
||||
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
|
||||
|
||||
public StarFissureBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BaseEntityBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new StarFissureBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
|
||||
if (entity.canUsePortal(false) && Shapes.joinIsNotEmpty(
|
||||
Shapes.create(entity.getBoundingBox().move(-pos.getX(), -pos.getY(), -pos.getZ())),
|
||||
state.getShape(level, pos), BooleanOp.AND)) {
|
||||
entity.setAsInsidePortal(this, pos);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DimensionTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
|
||||
if (level.dimension() == Level.OVERWORLD) return null;
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
if (overworld == null) return null;
|
||||
Vec3 destination = LinkingService.resolveDestination(overworld, overworld.getSharedSpawnPos(), true)
|
||||
.orElse(overworld.getSharedSpawnPos().getBottomCenter());
|
||||
level.playSound(null, pos, ModSounds.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
return new DimensionTransition(overworld, destination,
|
||||
entity.getDeltaMovement(), entity.getYRot(), entity.getXRot(),
|
||||
DimensionTransition.PLACE_PORTAL_TICKET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canBeReplaced(BlockState state, Fluid fluid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Reuses Minecraft's animated starfield renderer with a valid Rivencraft block-entity type. */
|
||||
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
|
||||
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.STAR_FISSURE.get(), pos, state);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
|
||||
import net.dnorberg.rivencraft.world.ScaledBiomeSource;
|
||||
import net.dnorberg.rivencraft.world.AgeFlatChunkGenerator;
|
||||
import net.dnorberg.rivencraft.world.AgeFeatureChunkGenerator;
|
||||
import net.dnorberg.rivencraft.world.StarFissureFeature;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
@@ -98,6 +99,13 @@ public final class AgeDimensionService {
|
||||
if (spawn == null) {
|
||||
applyInitialCelestialPhase(level, definition);
|
||||
spawn = createSafeSpawn(level, definition);
|
||||
if (AgeFeatureSelection.resolved(definition.plan()).stream()
|
||||
.anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES))
|
||||
|| definition.randomFeatures().stream()
|
||||
.anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES))) {
|
||||
StarFissureFeature.generate(level, definition.seed(), spawn,
|
||||
AgeBiomeSelection.isCave(runtimeProfile(definition)));
|
||||
}
|
||||
definition = AgeRegistrySavedData.get(level).setSpawn(definition.id(), spawn);
|
||||
}
|
||||
return Optional.of(new LinkTarget(
|
||||
|
||||
@@ -32,7 +32,9 @@ public final class AgeFeatureSelection {
|
||||
private static final List<ResourceLocation> FLUID_FEATURES = List.of(
|
||||
id("surface_lakes"), id("deep_lakes"), id("lava_lakes"));
|
||||
private static final long RANDOM_SKYLANDS_SALT = 0x534B594C414E4453L;
|
||||
private static final long RANDOM_STAR_FISSURES_SALT = 0x535441524752414DL;
|
||||
public static final ResourceLocation CRYSTAL_FORMATIONS = id("crystal_formations");
|
||||
public static final ResourceLocation STAR_FISSURES = id("star_fissures");
|
||||
|
||||
private AgeFeatureSelection() {}
|
||||
|
||||
@@ -176,12 +178,33 @@ public final class AgeFeatureSelection {
|
||||
choices.add(new Choice(id("skylands"), Optional.empty()));
|
||||
if (error(CRYSTAL_FORMATIONS, profile, registries) == null && random.nextInt(10) == 0)
|
||||
choices.add(new Choice(CRYSTAL_FORMATIONS, Optional.empty()));
|
||||
if (randomStarFissure(ageSeed))
|
||||
choices.add(new Choice(STAR_FISSURES, Optional.empty()));
|
||||
AgeBiomeSelection biomes = AgeBiomeSelection.compile(WritingGrammar.parse(OrderedWriting.EMPTY),
|
||||
AgeTraits.select(ageSeed), profile, ageSeed, registries);
|
||||
choices.addAll(AgeStructureSelection.randomChoices(ageSeed, profile, biomes, registries));
|
||||
return List.copyOf(choices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy's default FeatureSmalls0 grammar: stop without a feature (rank 4),
|
||||
* emit one (rank 4), or emit one and repeat (rank 2). Its rank-derived
|
||||
* weights are 1:1:3. Within FEATURE_SMALL, the Star Fissure rule has weight
|
||||
* 1 of 9: three rank-3 choices have weight 1 each and two rank-1 lake
|
||||
* choices have weight 3 each. Other small features remain selected by this
|
||||
* port's existing random feature pipeline; only the fissure result is used.
|
||||
*/
|
||||
private static boolean randomStarFissure(long ageSeed) {
|
||||
Random random = new Random(mix(ageSeed ^ RANDOM_STAR_FISSURES_SALT));
|
||||
for (int step = 0; step < 27; step++) {
|
||||
int branch = random.nextInt(5);
|
||||
if (branch == 0) return false;
|
||||
if (random.nextInt(9) == 0) return true;
|
||||
if (branch == 1) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<ResourceLocation> shuffledMaterials(long ageSeed, ResourceLocation feature) {
|
||||
var category = solidFeature(feature)
|
||||
? BlockSymbolPages.CATEGORY : BlockSymbolPages.FLUID_CATEGORY;
|
||||
@@ -212,6 +235,7 @@ public final class AgeFeatureSelection {
|
||||
return "Feature " + choice + " needs exposed solid ground, not Cave or Void terrain";
|
||||
return null;
|
||||
}
|
||||
if (choice.equals(STAR_FISSURES)) return null;
|
||||
if (skylands(choice)) return null;
|
||||
if (tendrils(choice)) return null;
|
||||
if (spheres(choice)) return null;
|
||||
|
||||
@@ -79,11 +79,13 @@ public final class AgePersistenceGameTests {
|
||||
"A random Age may roll at most two distinct structure features");
|
||||
for (AgeFeatureSelection.Choice choice : age.randomFeatures()) {
|
||||
helper.assertTrue(AgeFeatureSelection.skylands(choice.id())
|
||||
|| choice.id().equals(AgeFeatureSelection.CRYSTAL_FORMATIONS)
|
||||
|| choice.id().equals(AgeFeatureSelection.STAR_FISSURES)
|
||||
|| AgeStructureSelection.feature(choice.id())
|
||||
? choice.material().isEmpty()
|
||||
: choice.material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choice) == null,
|
||||
"Random material features need valid blocks or fluids; Skylands and structures need none");
|
||||
"Random material features need valid blocks or fluids; material-free features need none");
|
||||
}
|
||||
}
|
||||
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
|
||||
|
||||
@@ -40,7 +40,8 @@ public final class FeatureWritingGameTests {
|
||||
"feature/bastions", "feature/buried_treasure", "feature/shipwrecks",
|
||||
"feature/ocean_ruins", "feature/pillager_outposts", "feature/ruined_portals",
|
||||
"feature/nether_fossils", "feature/ancient_cities", "feature/trail_ruins",
|
||||
"feature/dense_ores", "feature/obelisks", "feature/crystal_formations");
|
||||
"feature/dense_ores", "feature/obelisks", "feature/crystal_formations",
|
||||
"feature/star_fissures");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
|
||||
"Supported feature pages must appear in the creative and JEI symbol feed");
|
||||
for (String path : pages) {
|
||||
@@ -74,6 +75,7 @@ public final class FeatureWritingGameTests {
|
||||
|| AgeFeatureSelection.spikes(choice.id())
|
||||
|| AgeFeatureSelection.obelisks(choice.id())
|
||||
|| choice.id().equals(AgeFeatureSelection.CRYSTAL_FORMATIONS)
|
||||
|| choice.id().equals(AgeFeatureSelection.STAR_FISSURES)
|
||||
|| choice.id().equals(AgeDenseOres.ID)) continue;
|
||||
var placement = AgeFeatureSelection.placedFeature(choice.id());
|
||||
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
@@ -187,8 +189,9 @@ public final class FeatureWritingGameTests {
|
||||
helper.assertTrue(random.stream().allMatch(choice -> AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id())
|
||||
|| choice.id().equals(AgeFeatureSelection.STAR_FISSURES)
|
||||
|| AgeStructureSelection.feature(choice.id())),
|
||||
"Cave random Ages must not choose incompatible surface features");
|
||||
"Cave random Ages must only choose compatible features, including Star Fissures");
|
||||
if (random.stream().anyMatch(choice -> AgeFeatureSelection.tendrils(choice.id()))) found = true;
|
||||
}
|
||||
helper.assertTrue(found, "A page-free random Cave Age must be able to roll Tendrils");
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class ClientEvents {
|
||||
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
|
||||
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
|
||||
event.registerBlockEntityRenderer(ModBlockEntities.WRITING_DESK.get(), WritingDeskRenderer::new);
|
||||
event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.TheEndPortalRenderer;
|
||||
|
||||
/** Uses the modern end-portal starfield shader at Legacy's full-block face positions. */
|
||||
public final class StarFissureRenderer extends TheEndPortalRenderer<StarFissureBlockEntity> {
|
||||
public StarFissureRenderer(BlockEntityRendererProvider.Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getOffsetUp() {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getOffsetDown() {
|
||||
return 1.0F;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
@@ -28,6 +29,9 @@ public final class ModBlockEntities {
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<InkMixerBlockEntity>> INK_MIXER =
|
||||
BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder
|
||||
.of(InkMixerBlockEntity::new, ModBlocks.INK_MIXER.get()).build(null));
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<StarFissureBlockEntity>> STAR_FISSURE =
|
||||
BLOCK_ENTITIES.register("star_fissure", () -> BlockEntityType.Builder
|
||||
.of(StarFissureBlockEntity::new, ModBlocks.STAR_FISSURE.get()).build(null));
|
||||
|
||||
private ModBlockEntities() {
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.block.BookBinderBlock;
|
||||
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
||||
import net.dnorberg.rivencraft.block.StarFissureBlock;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -52,6 +53,9 @@ public final class ModBlocks {
|
||||
public static final DeferredBlock<AmethystClusterBlock> SMALL_CRYSTAL_BUD = BLOCKS.registerBlock("small_crystal_bud",
|
||||
properties -> new AmethystClusterBlock(3.0F, 4.0F, properties),
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.SMALL_AMETHYST_BUD));
|
||||
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE = BLOCKS.registerBlock("star_fissure",
|
||||
StarFissureBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL)
|
||||
.lightLevel(state -> 6));
|
||||
|
||||
private ModBlocks() {
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public final class ModSounds {
|
||||
DeferredRegister.create(Registries.SOUND_EVENT, Rivencraft.MOD_ID);
|
||||
|
||||
public static final DeferredHolder<SoundEvent, SoundEvent> LINK = register("linking.link");
|
||||
public static final DeferredHolder<SoundEvent, SoundEvent> LINK_FISSURE = register("linking.link-fissure");
|
||||
|
||||
private ModSounds() {
|
||||
}
|
||||
|
||||
@@ -155,6 +155,8 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
@Override public void applyCarvers(WorldGenRegion level, long seed, RandomState random, BiomeManager biomes,
|
||||
StructureManager structures, ChunkAccess chunk, GenerationStep.Carving step) { }
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { }
|
||||
// Keep vanilla FlatLevelSource's generator API values. Actual layer
|
||||
// placement uses chunk.getMinBuildHeight() above, not this metadata value.
|
||||
@Override public int getMinY() { return 0; }
|
||||
@Override public int getGenDepth() { return 384; }
|
||||
@Override public int getSeaLevel() { return -63; }
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* Ports the footprint and carving of Mystcraft's WorldGenMystStarFissure.
|
||||
* The original is LGPL-3.0, copyright XCompWiz and Mystcraft contributors.
|
||||
*/
|
||||
public final class StarFissureFeature {
|
||||
private static final long SALT = 0x5354415246495353L;
|
||||
|
||||
private StarFissureFeature() {}
|
||||
|
||||
/** Legacy chooses a point 8–23 blocks inside/after the spawn chunk, then grows the rift along +Z. */
|
||||
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
|
||||
long chunkX = spawn.getX() >> 4;
|
||||
long chunkZ = spawn.getZ() >> 4;
|
||||
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
|
||||
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
|
||||
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
|
||||
int length = random.nextInt(8) + 10;
|
||||
int[][] intervals = new int[length][2];
|
||||
List<BlockPos> columns = new ArrayList<>();
|
||||
addRow(columns, originX, originZ, 0, 0, 0);
|
||||
for (int row = 1; row < length; row++) {
|
||||
int range = length + Math.min(row, length - row) + 1;
|
||||
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
|
||||
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
|
||||
if (left > right) {
|
||||
int swap = left;
|
||||
left = right;
|
||||
right = swap;
|
||||
}
|
||||
left = Math.min(left, intervals[row - 1][1]);
|
||||
right = Math.max(right, intervals[row - 1][0]);
|
||||
intervals[row][0] = left;
|
||||
intervals[row][1] = right;
|
||||
addRow(columns, originX, originZ, row, left, right);
|
||||
}
|
||||
return List.copyOf(columns);
|
||||
}
|
||||
|
||||
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
|
||||
for (int x = left; x <= right; x++) {
|
||||
// Footprint is a 2D column map; this placeholder Y is never used
|
||||
// for placement. generate() resolves the real floor from the level.
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
|
||||
}
|
||||
}
|
||||
|
||||
public static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
|
||||
List<BlockPos> columns = footprint(ageSeed, spawn);
|
||||
// Legacy's y=0 was its minimum build height. The equivalent in 1.21.1
|
||||
// is the dimension's minimum Y, including the bedrock layer itself.
|
||||
int floorY = level.getMinBuildHeight();
|
||||
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||
for (BlockPos column : columns) {
|
||||
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
|
||||
if (caveTerrain) {
|
||||
// Cave terrain has a bedrock roof. Open the fissure into the
|
||||
// cavern, but never punch through its upper boundary.
|
||||
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
|
||||
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
|
||||
top = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear the full column first, including water. Place the portal
|
||||
// last so an existing fluid cannot occupy its block during carving.
|
||||
for (int y = floorY + 1; y < top; y++) {
|
||||
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
|
||||
}
|
||||
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
|
||||
ModBlocks.STAR_FISSURE.get().defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Pure selection and footprint checks; runtime carving is left to the in-game visual check. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class StarFissureGameTests {
|
||||
private StarFissureGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredFissureIsWritableAndSaved(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("feature/star_fissures"),
|
||||
"Star Fissures must appear in the creative and JEI page feed");
|
||||
helper.assertTrue(PageItem.isUsableWriting(PageItem.createBundledCreativePage("feature/star_fissures")),
|
||||
"The page must be usable in an authored book");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("terrain/standard"), page("feature/star_fissures")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 58L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(58L), registries);
|
||||
var resolved = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(resolved.size(), 1, "One authored fissure must remain one feature choice");
|
||||
helper.assertValueEqual(resolved.getFirst().id(), AgeFeatureSelection.STAR_FISSURES,
|
||||
"The authored feature must be a Star Fissure");
|
||||
helper.assertTrue(resolved.getFirst().material().isEmpty(), "Star Fissures consume no material page");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), resolved,
|
||||
"The feature must survive Age-plan save and load");
|
||||
helper.assertTrue(ModBlocks.STAR_FISSURE.get().defaultBlockState() != null,
|
||||
"The non-obtainable star portal block must be registered");
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat", "cave", "void"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(AgeFeatureSelection.STAR_FISSURES,
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) == null,
|
||||
"Legacy fissures can appear on " + terrain + " terrain");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void randomFissuresAndSpawnFootprintAreStable(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
int selected = 0;
|
||||
for (long seed = 0; seed < 128; seed++) {
|
||||
boolean standard = hasRandomFissure(seed, AgeGenerationProfiles.STANDARD, registries);
|
||||
if (standard) selected++;
|
||||
for (String terrain : List.of("cave", "void"))
|
||||
helper.assertValueEqual(hasRandomFissure(seed, id(terrain), registries), standard,
|
||||
"Legacy small-feature selection must not depend on terrain");
|
||||
}
|
||||
helper.assertTrue(selected > 0 && selected < 128,
|
||||
"The weighted Legacy grammar should select fissures in some, not all, random Ages");
|
||||
BlockPos spawn = new BlockPos(0, 64, 0);
|
||||
var footprint = StarFissureFeature.footprint(58L, spawn);
|
||||
helper.assertValueEqual(footprint, StarFissureFeature.footprint(58L, spawn),
|
||||
"The Age seed and spawn must reproduce the same fissure");
|
||||
int minZ = footprint.stream().mapToInt(BlockPos::getZ).min().orElseThrow();
|
||||
int maxZ = footprint.stream().mapToInt(BlockPos::getZ).max().orElseThrow();
|
||||
helper.assertTrue(minZ >= 8 && minZ <= 23,
|
||||
"Legacy starts 8–23 blocks past the spawn chunk's Z edge");
|
||||
helper.assertTrue(maxZ - minZ + 1 >= 20 && maxZ - minZ + 1 <= 34,
|
||||
"Legacy's rift spans 10–17 paired Z rows");
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
int row = z;
|
||||
var xs = footprint.stream().filter(pos -> pos.getZ() == row)
|
||||
.mapToInt(BlockPos::getX).sorted().toArray();
|
||||
helper.assertTrue(xs.length > 0, "Every Z row must contain portal tiles");
|
||||
for (int i = 1; i < xs.length; i++)
|
||||
helper.assertValueEqual(xs[i], xs[i - 1] + 1,
|
||||
"Each jagged Legacy row remains contiguous");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
}
|
||||
|
||||
private static boolean hasRandomFissure(long seed, ResourceLocation terrain,
|
||||
net.minecraft.core.RegistryAccess registries) {
|
||||
return AgeFeatureSelection.randomChoices(seed, AgeRuntimeProfiles.resolve(terrain), registries).stream()
|
||||
.anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES));
|
||||
}
|
||||
|
||||
private static WritingEntry page(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(id(path)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"variants":{"":{"model":"rivencraft:block/star_fissure"}}}
|
||||
@@ -289,6 +289,9 @@
|
||||
"symbol.rivencraft.feature.obelisks.description": "Add rare tall obelisks on exposed solid ground, with a random or selected block material.",
|
||||
"symbol.rivencraft.feature.crystal_formations": "Crystal Formations",
|
||||
"symbol.rivencraft.feature.crystal_formations.description": "Add blue crystal formations whose budding blocks grow harvestable clusters.",
|
||||
"symbol.rivencraft.feature.star_fissures": "Star Fissures",
|
||||
"symbol.rivencraft.feature.star_fissures.description": "Open a starry fissure near this Age's spawn that links back to the Overworld.",
|
||||
"block.rivencraft.star_fissure": "Star Fissure",
|
||||
"block.rivencraft.crystal_block": "Crystal Block",
|
||||
"block.rivencraft.budding_crystal": "Budding Crystal",
|
||||
"block.rivencraft.small_crystal_bud": "Small Crystal Bud",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:block/end_portal"}
|
||||
@@ -6,5 +6,9 @@
|
||||
"name": "rivencraft:linking/link"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linking.link-fissure": {
|
||||
"category": "player",
|
||||
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.star_fissures",
|
||||
"description": "symbol.rivencraft.feature.star_fissures.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:star_fissures" },
|
||||
"poem": "Star Fissure World Home"
|
||||
}
|
||||
Reference in new issue
Block a user