Public Access
Port bounded colored decay and align Legacy defaults (#68)
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
066ef2b758
commit
a9a8b3cdd5
40 files changed
+698
-27
No files matched your search
@@ -23,6 +23,7 @@ import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.book.AgeWeatherService;
|
||||
import net.dnorberg.rivencraft.book.AgeInstabilityRuntime;
|
||||
import net.dnorberg.rivencraft.book.AgeStatusEffectProviders;
|
||||
import net.dnorberg.rivencraft.book.AgeDecayProviders;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
@@ -40,6 +41,7 @@ public final class Rivencraft {
|
||||
public Rivencraft(IEventBus modEventBus, ModContainer modContainer) {
|
||||
AgeDimensionService.register();
|
||||
AgeStatusEffectProviders.register();
|
||||
AgeDecayProviders.register();
|
||||
modContainer.registerConfig(ModConfig.Type.SERVER, RivencraftConfig.SPEC);
|
||||
modContainer.registerConfig(ModConfig.Type.CLIENT, RivencraftClientConfig.SPEC);
|
||||
modEventBus.addListener(ModNetworking::register);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Solid footing for Age arrivals, rendered as a translucent cloud. */
|
||||
public final class CloudPlatformBlock extends Block {
|
||||
public CloudPlatformBlock(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean skipRendering(BlockState state, BlockState adjacent, Direction side) {
|
||||
return adjacent.is(this) || super.skipRendering(state, adjacent, side);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean propagatesSkylightDown(BlockState state, BlockGetter level, BlockPos pos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLightBlock(BlockState state, BlockGetter level, BlockPos pos) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeInstabilityRuntime;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty;
|
||||
|
||||
/** The four colored decay variants selected by the current instability deck. */
|
||||
public final class DecayBlock extends Block {
|
||||
public static final EnumProperty<Color> COLOR = EnumProperty.create("color", Color.class);
|
||||
|
||||
public DecayBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(COLOR, Color.BLUE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(COLOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) {
|
||||
if (!level.isClientSide && state.getValue(COLOR) == Color.WHITE
|
||||
&& RivencraftConfig.decayEnabled() && RivencraftConfig.instabilityWorldMutation()
|
||||
&& AgeInstabilityRuntime.isAgeDimension(level.dimension().location())) {
|
||||
entity.hurt(level.damageSources().magic(), 1.0F);
|
||||
}
|
||||
super.stepOn(level, pos, state, entity);
|
||||
}
|
||||
|
||||
public enum Color implements StringRepresentable {
|
||||
BLUE, PURPLE, RED, WHITE;
|
||||
|
||||
@Override
|
||||
public String getSerializedName() {
|
||||
return name().toLowerCase(java.util.Locale.ROOT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.DecayBlock;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class AgeDecayGameTests {
|
||||
private AgeDecayGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void frontierRoundTripsWithoutDuplicates(GameTestHelper helper) {
|
||||
AgeDecaySavedData data = new AgeDecaySavedData();
|
||||
BlockPos first = new BlockPos(3, 70, 5);
|
||||
data.add(DecayBlock.Color.BLUE, first);
|
||||
data.add(DecayBlock.Color.BLUE, first);
|
||||
data.add(DecayBlock.Color.WHITE, new BlockPos(-4, 64, 9));
|
||||
helper.assertValueEqual(data.frontier(DecayBlock.Color.BLUE).size(), 1,
|
||||
"One frontier block must not be stored twice");
|
||||
helper.assertValueEqual(data.nextAttempt(), 0L, "First attempt starts at zero");
|
||||
AgeDecaySavedData restored = AgeDecaySavedData.load(
|
||||
data.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(restored.nextAttempt(), 1L, "Attempt sequence survives reload");
|
||||
helper.assertValueEqual(restored.frontier(DecayBlock.Color.BLUE).getFirst(), first,
|
||||
"Blue frontier survives reload");
|
||||
helper.assertValueEqual(restored.frontier(DecayBlock.Color.WHITE).size(), 1,
|
||||
"White frontier survives reload independently");
|
||||
restored.remove(DecayBlock.Color.BLUE, first);
|
||||
helper.assertTrue(restored.frontier(DecayBlock.Color.BLUE).isEmpty(),
|
||||
"Removed frontier is not scheduled for further spread");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void targetMustRemainInChosenLoadedChunk(GameTestHelper helper) {
|
||||
BlockPos center = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
ChunkPos chunk = new ChunkPos(center);
|
||||
helper.assertTrue(AgeDecayProviders.isLoadedTarget(helper.getLevel(), List.of(chunk), center),
|
||||
"An already-loaded target inside the selected chunk is eligible");
|
||||
BlockPos outside = new BlockPos((chunk.x + 1) << 4, center.getY(), center.getZ());
|
||||
helper.assertTrue(!AgeDecayProviders.isLoadedTarget(helper.getLevel(), List.of(chunk), outside),
|
||||
"Spread cannot cross into a chunk absent from this Age run");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void overworldCannotRunDecayProvider(GameTestHelper helper) {
|
||||
UUID ageId = UUID.randomUUID();
|
||||
AgeDefinition age = new AgeDefinition(ageId, "Decay isolation", 100L, AgeGenerationProfiles.STANDARD,
|
||||
AgeDefinition.CURRENT_GENERATION_VERSION, 0L, AgeDefinition.dimensionId(ageId), null,
|
||||
AgeTraits.LEGACY_DEFAULTS);
|
||||
AgeInstabilityRuntime.Budget budget = new AgeInstabilityRuntime.Budget(1);
|
||||
var context = new AgeInstabilityRuntime.Context(helper.getLevel(), age,
|
||||
List.of(new ChunkPos(helper.absolutePos(new BlockPos(1, 1, 1)))),
|
||||
helper.getLevel().getServer().getTickCount(), budget);
|
||||
AgeDecayProviders.apply(context, ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "decayblue"), 1);
|
||||
helper.assertValueEqual(budget.remaining(), 1, "Overworld decay cannot claim an Age action");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void savedSpawnAndPlatformAreProtected(GameTestHelper helper) {
|
||||
UUID ageId = UUID.randomUUID();
|
||||
BlockPos spawn = new BlockPos(0, 65, 0);
|
||||
AgeDefinition age = new AgeDefinition(ageId, "Protected arrival", 100L, AgeGenerationProfiles.STANDARD,
|
||||
AgeDefinition.CURRENT_GENERATION_VERSION, 0L, AgeDefinition.dimensionId(ageId), spawn,
|
||||
AgeTraits.LEGACY_DEFAULTS);
|
||||
helper.assertTrue(AgeDecayProviders.isSpawnProtected(age, spawn.below().offset(2, 0, 2)),
|
||||
"Arrival platform must not decay");
|
||||
helper.assertTrue(AgeDecayProviders.isSpawnProtected(age, spawn.offset(4, 0, 4)),
|
||||
"The protective margin must not decay");
|
||||
helper.assertTrue(!AgeDecayProviders.isSpawnProtected(age, spawn.offset(5, 0, 0)),
|
||||
"Spawn protection is bounded");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.DecayBlock;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.common.util.BlockSnapshot;
|
||||
import net.neoforged.neoforge.common.util.FakePlayerFactory;
|
||||
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||
|
||||
/** Bounded, per-Age decay seeding and six-direction Legacy-style spreading. */
|
||||
public final class AgeDecayProviders {
|
||||
public static final TagKey<Block> PROTECTED = TagKey.create(Registries.BLOCK,
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "decay_protected"));
|
||||
private static final Map<ResourceLocation, DecayBlock.Color> COLORS = Map.of(
|
||||
id("decayblue"), DecayBlock.Color.BLUE,
|
||||
id("decaypurple"), DecayBlock.Color.PURPLE,
|
||||
id("decayred"), DecayBlock.Color.RED,
|
||||
id("decaywhite"), DecayBlock.Color.WHITE);
|
||||
private static final Direction[] DIRECTIONS = Direction.values();
|
||||
|
||||
private AgeDecayProviders() {}
|
||||
|
||||
public static void register() {
|
||||
COLORS.keySet().forEach(id -> AgeInstabilityRuntime.register(id, true, AgeDecayProviders::apply));
|
||||
}
|
||||
|
||||
/** Operator test hook; never changes the Age's saved instability score or provider plan. */
|
||||
public static int pulseForTesting(ServerPlayer player, DecayBlock.Color color, int count) {
|
||||
ServerLevel level = player.serverLevel();
|
||||
if (!RivencraftConfig.decayEnabled() || !RivencraftConfig.instabilityWorldMutation()
|
||||
|| !AgeInstabilityRuntime.isAgeDimension(level.dimension().location())
|
||||
|| !level.getChunkSource().hasChunk(player.chunkPosition().x, player.chunkPosition().z)) return -1;
|
||||
AgeDefinition age = AgeRegistrySavedData.get(level).findByDimension(level.dimension().location()).orElse(null);
|
||||
if (age == null) return -1;
|
||||
ResourceLocation id = id("decay" + color.getSerializedName());
|
||||
int changed = 0;
|
||||
for (int i = 0; i < Math.min(count, 32); i++) {
|
||||
AgeInstabilityRuntime.Budget budget = new AgeInstabilityRuntime.Budget(1);
|
||||
apply(new AgeInstabilityRuntime.Context(level, age, List.of(player.chunkPosition()),
|
||||
level.getServer().getTickCount(), budget), id, 1);
|
||||
changed += 1 - budget.remaining();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
static void apply(AgeInstabilityRuntime.Context context, ResourceLocation id, int level) {
|
||||
if (!RivencraftConfig.decayEnabled() || !RivencraftConfig.instabilityWorldMutation()
|
||||
|| !AgeInstabilityRuntime.isAgeDimension(context.level().dimension().location())
|
||||
|| !context.age().dimensionId().equals(context.level().dimension().location())
|
||||
|| context.loadedChunks().isEmpty()) return;
|
||||
DecayBlock.Color color = COLORS.get(id);
|
||||
if (color == null) return;
|
||||
AgeDecaySavedData data = AgeDecaySavedData.get(context.level());
|
||||
Random random = new Random(context.age().seed() ^ data.nextAttempt() * 0x9E3779B97F4A7C15L
|
||||
^ id.hashCode());
|
||||
List<BlockPos> frontier = data.frontier(color);
|
||||
BlockPos source = selectLoadedSource(context.level(), context.loadedChunks(), frontier, random);
|
||||
if (source == null) {
|
||||
seed(context, color, data, random);
|
||||
return;
|
||||
}
|
||||
BlockState sourceState = context.level().getBlockState(source);
|
||||
if (!sourceState.is(ModBlocks.DECAY.get()) || sourceState.getValue(DecayBlock.COLOR) != color) {
|
||||
data.remove(color, source);
|
||||
return;
|
||||
}
|
||||
for (int attempt = 0; attempt < RivencraftConfig.decayMaxAttempts(); attempt++) {
|
||||
BlockPos target = source.relative(DIRECTIONS[random.nextInt(DIRECTIONS.length)]);
|
||||
if (!isLoadedTarget(context.level(), context.loadedChunks(), target)
|
||||
|| isSpawnProtected(context.age(), target)) continue;
|
||||
BlockState prior = context.level().getBlockState(target);
|
||||
if (!canReplace(context.level(), target, prior, color)) continue;
|
||||
int difficulty = conversionDifficulty(context.level(), target, prior, color);
|
||||
if (random.nextInt(difficulty) != 0) continue;
|
||||
if (replace(context, target, prior, color)) data.add(color, target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void seed(AgeInstabilityRuntime.Context context, DecayBlock.Color color,
|
||||
AgeDecaySavedData data, Random random) {
|
||||
ChunkPos chunk = context.loadedChunks().get(random.nextInt(context.loadedChunks().size()));
|
||||
int x = chunk.getMinBlockX() + random.nextInt(16);
|
||||
int z = chunk.getMinBlockZ() + random.nextInt(16);
|
||||
BlockPos pos = new BlockPos(x,
|
||||
context.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1, z);
|
||||
if (!isLoadedTarget(context.level(), context.loadedChunks(), pos)
|
||||
|| isSpawnProtected(context.age(), pos)) return;
|
||||
BlockState prior = context.level().getBlockState(pos);
|
||||
if (canReplace(context.level(), pos, prior, color) && replace(context, pos, prior, color)) {
|
||||
data.add(color, pos);
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockPos selectLoadedSource(ServerLevel level, List<ChunkPos> chunks,
|
||||
List<BlockPos> frontier, Random random) {
|
||||
if (frontier.isEmpty()) return null;
|
||||
int start = random.nextInt(frontier.size());
|
||||
for (int i = 0; i < Math.min(frontier.size(), 16); i++) {
|
||||
BlockPos pos = frontier.get((start + i) % frontier.size());
|
||||
if (isLoadedTarget(level, chunks, pos)) return pos;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean isLoadedTarget(ServerLevel level, List<ChunkPos> chunks, BlockPos pos) {
|
||||
return level.isInWorldBounds(pos) && level.getChunkSource().hasChunk(pos.getX() >> 4, pos.getZ() >> 4)
|
||||
&& chunks.contains(new ChunkPos(pos));
|
||||
}
|
||||
|
||||
static boolean isSpawnProtected(AgeDefinition age, BlockPos pos) {
|
||||
BlockPos spawn = age.spawn();
|
||||
return spawn != null && Math.abs(pos.getX() - spawn.getX()) <= 4
|
||||
&& Math.abs(pos.getZ() - spawn.getZ()) <= 4
|
||||
&& pos.getY() >= spawn.getY() - 2 && pos.getY() <= spawn.getY() + 4;
|
||||
}
|
||||
|
||||
static boolean canReplace(ServerLevel level, BlockPos pos, BlockState prior, DecayBlock.Color color) {
|
||||
if (!AgeInstabilityRuntime.isAgeDimension(level.dimension().location())
|
||||
|| !level.isInWorldBounds(pos) || !level.getChunkSource().hasChunk(pos.getX() >> 4, pos.getZ() >> 4)
|
||||
|| prior.is(PROTECTED) || prior.is(BlockTags.PORTALS)
|
||||
|| prior.hasBlockEntity() || level.getBlockEntity(pos) != null
|
||||
|| prior.getDestroySpeed(level, pos) < 0 || !prior.getFluidState().isEmpty()
|
||||
|| isCritical(prior)) return false;
|
||||
return !prior.is(ModBlocks.DECAY.get()) || prior.getValue(DecayBlock.COLOR) != color;
|
||||
}
|
||||
|
||||
private static boolean isCritical(BlockState prior) {
|
||||
return prior.is(Blocks.BEDROCK) || prior.is(Blocks.BARRIER)
|
||||
|| prior.is(Blocks.END_PORTAL) || prior.is(Blocks.END_PORTAL_FRAME)
|
||||
|| prior.is(Blocks.END_GATEWAY) || prior.is(Blocks.NETHER_PORTAL)
|
||||
|| prior.is(Blocks.COMMAND_BLOCK) || prior.is(Blocks.CHAIN_COMMAND_BLOCK)
|
||||
|| prior.is(Blocks.REPEATING_COMMAND_BLOCK) || prior.is(Blocks.STRUCTURE_BLOCK)
|
||||
|| prior.is(Blocks.JIGSAW) || prior.is(Blocks.LIGHT)
|
||||
|| prior.is(Blocks.REINFORCED_DEEPSLATE)
|
||||
|| prior.getBlock() != ModBlocks.DECAY.get()
|
||||
&& net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(prior.getBlock())
|
||||
.getNamespace().equals(Rivencraft.MOD_ID);
|
||||
}
|
||||
|
||||
static int conversionDifficulty(ServerLevel level, BlockPos pos, BlockState state, DecayBlock.Color color) {
|
||||
if (state.isAir()) return switch (color) {
|
||||
case BLUE, RED -> 20;
|
||||
case PURPLE -> 8;
|
||||
case WHITE -> 50;
|
||||
};
|
||||
if (state.is(ModBlocks.DECAY.get())) return color == DecayBlock.Color.WHITE ? 1 : 5;
|
||||
float hardness = Math.min(1000.0F, Math.max(0.0F, state.getDestroySpeed(level, pos)));
|
||||
return switch (color) {
|
||||
case BLUE -> Math.max(1, (int) hardness * 2);
|
||||
case RED -> Math.max(1, (int) hardness);
|
||||
case PURPLE -> Math.max(1, ((int) hardness + 1) * 10);
|
||||
case WHITE -> 1;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean replace(AgeInstabilityRuntime.Context context, BlockPos pos, BlockState prior,
|
||||
DecayBlock.Color color) {
|
||||
ServerLevel level = context.level();
|
||||
// A fake player gives claim/protection listeners the Age and exact block position.
|
||||
var actor = FakePlayerFactory.getMinecraft(level);
|
||||
actor.setPos(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
|
||||
if (!prior.isAir()) {
|
||||
var breakEvent = new BlockEvent.BreakEvent(level, pos, prior, actor);
|
||||
NeoForge.EVENT_BUS.post(breakEvent);
|
||||
if (breakEvent.isCanceled()) return false;
|
||||
}
|
||||
if (level.getBlockState(pos) != prior || !context.budget().claim()) return false;
|
||||
var snapshot = BlockSnapshot.create(level.dimension(), level, pos);
|
||||
BlockState decay = ModBlocks.DECAY.get().defaultBlockState().setValue(DecayBlock.COLOR, color);
|
||||
if (!level.setBlock(pos, decay, 3)) return false;
|
||||
var placeEvent = new BlockEvent.EntityPlaceEvent(snapshot, prior, actor);
|
||||
NeoForge.EVENT_BUS.post(placeEvent);
|
||||
if (placeEvent.isCanceled()) {
|
||||
snapshot.restore();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.block.DecayBlock;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
/** Small, per-Age frontier and deterministic attempt counter; never stores chunk references. */
|
||||
public final class AgeDecaySavedData extends SavedData {
|
||||
private static final int MAX_FRONTIER_PER_COLOR = 1024;
|
||||
private static final Factory<AgeDecaySavedData> FACTORY = new Factory<>(AgeDecaySavedData::new,
|
||||
AgeDecaySavedData::load, DataFixTypes.SAVED_DATA_MAP_DATA);
|
||||
private final EnumMap<DecayBlock.Color, List<BlockPos>> frontiers = new EnumMap<>(DecayBlock.Color.class);
|
||||
private long attempts;
|
||||
|
||||
public AgeDecaySavedData() {
|
||||
for (DecayBlock.Color color : DecayBlock.Color.values()) frontiers.put(color, new ArrayList<>());
|
||||
}
|
||||
|
||||
public static AgeDecaySavedData get(ServerLevel level) {
|
||||
return level.getDataStorage().computeIfAbsent(FACTORY, "rivencraft_decay");
|
||||
}
|
||||
|
||||
public long nextAttempt() {
|
||||
setDirty();
|
||||
return attempts++;
|
||||
}
|
||||
|
||||
public List<BlockPos> frontier(DecayBlock.Color color) {
|
||||
return frontiers.get(color);
|
||||
}
|
||||
|
||||
public void add(DecayBlock.Color color, BlockPos pos) {
|
||||
List<BlockPos> frontier = frontiers.get(color);
|
||||
if (frontier.contains(pos)) return;
|
||||
if (frontier.size() >= MAX_FRONTIER_PER_COLOR) frontier.remove(0);
|
||||
frontier.add(pos.immutable());
|
||||
setDirty();
|
||||
}
|
||||
|
||||
public void remove(DecayBlock.Color color, BlockPos pos) {
|
||||
if (frontiers.get(color).remove(pos)) setDirty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
tag.putLong("Attempts", attempts);
|
||||
for (DecayBlock.Color color : DecayBlock.Color.values()) {
|
||||
ListTag positions = new ListTag();
|
||||
for (BlockPos pos : frontiers.get(color)) {
|
||||
CompoundTag entry = new CompoundTag();
|
||||
entry.putInt("X", pos.getX());
|
||||
entry.putInt("Y", pos.getY());
|
||||
entry.putInt("Z", pos.getZ());
|
||||
positions.add(entry);
|
||||
}
|
||||
tag.put(color.getSerializedName(), positions);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
static AgeDecaySavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AgeDecaySavedData data = new AgeDecaySavedData();
|
||||
data.attempts = Math.max(0L, tag.getLong("Attempts"));
|
||||
for (DecayBlock.Color color : DecayBlock.Color.values()) {
|
||||
ListTag positions = tag.getList(color.getSerializedName(), CompoundTag.TAG_COMPOUND);
|
||||
for (int index = 0; index < Math.min(positions.size(), MAX_FRONTIER_PER_COLOR); index++) {
|
||||
CompoundTag entry = positions.getCompound(index);
|
||||
data.frontiers.get(color).add(new BlockPos(entry.getInt("X"), entry.getInt("Y"), entry.getInt("Z")));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
||||
import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility;
|
||||
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||
@@ -228,7 +229,7 @@ public final class AgeDimensionService {
|
||||
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);
|
||||
level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 2);
|
||||
for (int y = 1; y <= 4; y++) {
|
||||
level.setBlock(floor.above(y), Blocks.AIR.defaultBlockState(), 2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class CloudPlatformGameTests {
|
||||
private CloudPlatformGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void cloudIsSolidFootingWithoutAnObtainableItem(GameTestHelper helper) {
|
||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
var state = ModBlocks.CLOUD_PLATFORM.get().defaultBlockState();
|
||||
helper.getLevel().setBlock(pos, state, 2);
|
||||
helper.assertTrue(!state.getCollisionShape(helper.getLevel(), pos).isEmpty(),
|
||||
"Cloud platform must support a standing player");
|
||||
helper.assertTrue(ModBlocks.CLOUD_PLATFORM.get().getCloneItemStack(helper.getLevel(), pos, state).isEmpty(),
|
||||
"Cloud platform must not provide a pick-block item");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -250,7 +252,7 @@ public final class TerrainWritingGameTests {
|
||||
helper.assertTrue(level != null, "The written Age must create a runtime dimension");
|
||||
assertGenerator(helper, level, age.plan().runtimeProfile());
|
||||
BlockPos spawn = target.location().pos();
|
||||
helper.assertTrue(level.getBlockState(spawn.below()).is(Blocks.COBBLESTONE)
|
||||
helper.assertTrue(level.getBlockState(spawn.below()).is(ModBlocks.CLOUD_PLATFORM.get())
|
||||
&& level.getBlockState(spawn).isAir()
|
||||
&& level.getBlockState(spawn.above()).isAir(),
|
||||
"Every written terrain family must retain a safe arrival platform");
|
||||
|
||||
@@ -77,6 +77,7 @@ public final class ClientEvents {
|
||||
ItemBlockRenderTypes.setRenderLayer(ModBlocks.MEDIUM_CRYSTAL_BUD.get(), RenderType.cutout());
|
||||
ItemBlockRenderTypes.setRenderLayer(ModBlocks.LARGE_CRYSTAL_BUD.get(), RenderType.cutout());
|
||||
ItemBlockRenderTypes.setRenderLayer(ModBlocks.CRYSTAL_CLUSTER.get(), RenderType.cutout());
|
||||
ItemBlockRenderTypes.setRenderLayer(ModBlocks.CLOUD_PLATFORM.get(), RenderType.translucent());
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem);
|
||||
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package net.dnorberg.rivencraft.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import net.dnorberg.rivencraft.block.DecayBlock;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeDecayProviders;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.minecraft.commands.Commands;
|
||||
@@ -30,7 +33,20 @@ public final class RivencraftCommands {
|
||||
.executes(context -> unloadAge(
|
||||
context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension")
|
||||
))))));
|
||||
))))
|
||||
.then(Commands.literal("decay")
|
||||
.then(Commands.literal("pulse")
|
||||
.then(Commands.argument("color", StringArgumentType.word())
|
||||
.suggests((context, builder) -> {
|
||||
for (DecayBlock.Color color : DecayBlock.Color.values()) {
|
||||
builder.suggest(color.getSerializedName());
|
||||
}
|
||||
return builder.buildFuture();
|
||||
})
|
||||
.then(Commands.argument("attempts", IntegerArgumentType.integer(1, 32))
|
||||
.executes(context -> pulseDecay(context.getSource(),
|
||||
StringArgumentType.getString(context, "color"),
|
||||
IntegerArgumentType.getInteger(context, "attempts")))))))));
|
||||
}
|
||||
|
||||
private static int listAges(net.minecraft.commands.CommandSourceStack source) {
|
||||
@@ -80,4 +96,26 @@ public final class RivencraftCommands {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static int pulseDecay(net.minecraft.commands.CommandSourceStack source, String colorName, int attempts) {
|
||||
DecayBlock.Color color;
|
||||
try {
|
||||
color = DecayBlock.Color.valueOf(colorName.toUpperCase(java.util.Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
source.sendFailure(Component.literal("Unknown decay color: " + colorName));
|
||||
return 0;
|
||||
}
|
||||
if (source.getPlayer() == null) {
|
||||
source.sendFailure(Component.literal("Run this command as a player standing in an Age."));
|
||||
return 0;
|
||||
}
|
||||
int changed = AgeDecayProviders.pulseForTesting(source.getPlayer(), color, attempts);
|
||||
if (changed < 0) {
|
||||
source.sendFailure(Component.literal("Enter a loaded Age and enable instability.world_mutation and instability_decay.enabled first."));
|
||||
return 0;
|
||||
}
|
||||
source.sendSuccess(() -> Component.literal("Decay " + colorName + ": " + changed
|
||||
+ " block(s) converted in " + attempts + " bounded attempt(s)."), true);
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ public final class RivencraftConfig {
|
||||
private static final ModConfigSpec.IntValue STATUS_EFFECTS_MAX_ACTIVE;
|
||||
private static final ModConfigSpec.IntValue STATUS_EFFECTS_MAX_CANDIDATES;
|
||||
private static final ModConfigSpec.IntValue STATUS_EFFECTS_MAX_DURATION;
|
||||
private static final ModConfigSpec.BooleanValue DECAY_ENABLED;
|
||||
private static final ModConfigSpec.IntValue DECAY_MAX_ATTEMPTS;
|
||||
|
||||
static {
|
||||
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||
@@ -58,8 +60,8 @@ public final class RivencraftConfig {
|
||||
builder.push("instability");
|
||||
INSTABILITY_ENABLED = builder.comment("Run selected instability providers in loaded Ages. Disabling does not alter saved plans.")
|
||||
.define("enabled", true);
|
||||
INSTABILITY_WORLD_MUTATION = builder.comment("Allow providers to change blocks or create explosions. Off until world hazards are reviewed.")
|
||||
.define("world_mutation", false);
|
||||
INSTABILITY_WORLD_MUTATION = builder.comment("Allow selected instability providers to change blocks or create explosions, as in Legacy.")
|
||||
.define("world_mutation", true);
|
||||
INSTABILITY_COOLDOWN = builder.comment("Minimum ticks between instability runs for each loaded Age.")
|
||||
.defineInRange("cooldown_ticks", 20, 20, 1200);
|
||||
INSTABILITY_MAX_AGES = builder.comment("Maximum loaded Ages processed in one server tick.")
|
||||
@@ -89,6 +91,12 @@ public final class RivencraftConfig {
|
||||
STATUS_EFFECTS_MAX_DURATION = builder.comment("Maximum duration in ticks for one instability status effect.")
|
||||
.defineInRange("max_duration_ticks", 200, 20, 200);
|
||||
builder.pop();
|
||||
builder.push("instability_decay");
|
||||
DECAY_ENABLED = builder.comment("Allow destructive colored decay inside loaded Ages, as in Legacy. Also requires instability.world_mutation.")
|
||||
.define("enabled", true);
|
||||
DECAY_MAX_ATTEMPTS = builder.comment("Maximum six-neighbor decay conversion attempts per provider call; at most one block changes.")
|
||||
.defineInRange("max_attempts_per_call", 4, 1, 6);
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
@@ -126,4 +134,6 @@ public final class RivencraftConfig {
|
||||
public static int statusEffectsMaxActive() { return STATUS_EFFECTS_MAX_ACTIVE.get(); }
|
||||
public static int statusEffectsMaxCandidates() { return STATUS_EFFECTS_MAX_CANDIDATES.get(); }
|
||||
public static int statusEffectsMaxDuration() { return STATUS_EFFECTS_MAX_DURATION.get(); }
|
||||
public static boolean decayEnabled() { return DECAY_ENABLED.get(); }
|
||||
public static int decayMaxAttempts() { return DECAY_MAX_ATTEMPTS.get(); }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package net.dnorberg.rivencraft.gametest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class BookPageCapacityGameTests {
|
||||
private BookPageCapacityGameTests() {}
|
||||
|
||||
private static ItemStack page(String path) {
|
||||
return PageItem.createSymbolPage(new SymbolReference(
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path)));
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void binderAndNotebookRetainPagesBeyondOldLimit(GameTestHelper helper) {
|
||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
helper.getLevel().setBlock(pos, ModBlocks.BOOK_BINDER.get().defaultBlockState(), 3);
|
||||
BookBinderBlockEntity binder = (BookBinderBlockEntity) helper.getLevel().getBlockEntity(pos);
|
||||
binder.setTitle("Long Age");
|
||||
binder.setItem(BookBinderBlockEntity.COVER, new ItemStack(Items.LEATHER));
|
||||
binder.setItem(BookBinderBlockEntity.LINK_PANEL, PageItem.createLinkPanelPage());
|
||||
for (int index = 0; index < 32; index++) {
|
||||
helper.assertTrue(binder.insertPage(index, page("terrain/standard")),
|
||||
"Binder must accept page " + (index + 1));
|
||||
}
|
||||
binder.setItem(BookBinderBlockEntity.SIZE - 1, page("weather/normal"));
|
||||
BookBinderBlockEntity restored = new BookBinderBlockEntity(pos, binder.getBlockState());
|
||||
restored.setLevel(helper.getLevel());
|
||||
restored.loadWithComponents(binder.saveWithoutMetadata(helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(restored.nextPageIndex(), BookBinderBlockEntity.PAGE_COUNT,
|
||||
"Binder must retain its highest valid page position after reload");
|
||||
helper.assertTrue(restored.getItem(BookBinderBlockEntity.FIRST_PAGE + 31).is(ModItems.PAGE.get()),
|
||||
"Page 32 must survive reload");
|
||||
helper.assertTrue(restored.getItem(BookBinderBlockEntity.SIZE - 1).is(ModItems.PAGE.get()),
|
||||
"The final fixed-slot page must survive reload");
|
||||
helper.assertValueEqual(restored.writingGrammar().symbols().size(), 33,
|
||||
"Grammar must retain all authored pages");
|
||||
|
||||
List<ItemStack> notebookPages = new ArrayList<>();
|
||||
for (int index = 0; index < NotebookContents.MAX_PAGES; index++) notebookPages.add(ItemStack.EMPTY);
|
||||
notebookPages.set(31, page("terrain/standard"));
|
||||
notebookPages.set(NotebookContents.MAX_PAGES - 1, page("weather/normal"));
|
||||
NotebookContents notebook = new NotebookContents(notebookPages);
|
||||
helper.assertValueEqual(notebook.pages().size(), NotebookContents.MAX_PAGES,
|
||||
"Notebook must retain its highest valid page position");
|
||||
helper.assertValueEqual(notebook.pageCount(), 2,
|
||||
"Sparse Notebook contents must retain both pages");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.dnorberg.rivencraft.gametest;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
@@ -234,7 +236,7 @@ public final class LinkingGameTests {
|
||||
helper.assertValueEqual(ageLevel.getSeed(), definition.seed(),
|
||||
"The runtime dimension seed must match the persisted Age definition");
|
||||
BlockPos spawn = target.location().pos();
|
||||
helper.assertTrue(ageLevel.getBlockState(spawn.below()).is(Blocks.COBBLESTONE),
|
||||
helper.assertTrue(ageLevel.getBlockState(spawn.below()).is(ModBlocks.CLOUD_PLATFORM.get()),
|
||||
"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");
|
||||
@@ -285,7 +287,7 @@ public final class LinkingGameTests {
|
||||
var ageLevel = player.getServer().getLevel(destination.location().dimension());
|
||||
helper.assertTrue(ageLevel != null, profile + " must create a loaded runtime dimension");
|
||||
BlockPos spawn = destination.location().pos();
|
||||
helper.assertTrue(ageLevel != null && ageLevel.getBlockState(spawn.below()).is(Blocks.COBBLESTONE),
|
||||
helper.assertTrue(ageLevel != null && ageLevel.getBlockState(spawn.below()).is(ModBlocks.CLOUD_PLATFORM.get()),
|
||||
profile + " must create a solid arrival platform");
|
||||
helper.assertTrue(ageLevel != null && ageLevel.getBlockState(spawn).isAir()
|
||||
&& ageLevel.getBlockState(spawn.above()).isAir(),
|
||||
|
||||
@@ -23,11 +23,11 @@ import net.minecraft.world.item.Items;
|
||||
/** Fixed, ordered binder slots and a server-owned virtual result. */
|
||||
public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
public static final int RESULT = BookBinderBlockEntity.SIZE;
|
||||
public static final int SELECT_BASE = 100;
|
||||
public static final int MOVE_PREVIOUS = 200;
|
||||
public static final int MOVE_NEXT = 201;
|
||||
public static final int IMPORT_NOTEBOOK = 202;
|
||||
public static final int STRIP_CLICK_BASE = 300;
|
||||
public static final int SELECT_BASE = 1000;
|
||||
public static final int MOVE_PREVIOUS = 2000;
|
||||
public static final int MOVE_NEXT = 2001;
|
||||
public static final int IMPORT_NOTEBOOK = 2002;
|
||||
public static final int STRIP_CLICK_BASE = 3000;
|
||||
private static final int PLAYER_START = RESULT + 1;
|
||||
|
||||
private final BookBinderBlockEntity binder;
|
||||
|
||||
@@ -40,11 +40,9 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
this.openedHotbarSlot = inventory.selected;
|
||||
this.contents = new NotebookContainer(inventory, hand);
|
||||
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 9; column++) {
|
||||
// The client draws these pages on a filtered, scrollable surface instead.
|
||||
addSlot(new PageSlot(contents, column + row * 9, -1000, -1000));
|
||||
}
|
||||
for (int page = 0; page < NOTEBOOK_SLOTS; page++) {
|
||||
// The client draws these pages on a filtered, scrollable surface instead.
|
||||
addSlot(new PageSlot(contents, page, -1000, -1000));
|
||||
}
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 9; column++) {
|
||||
|
||||
@@ -7,6 +7,8 @@ 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.dnorberg.rivencraft.block.DecayBlock;
|
||||
import net.dnorberg.rivencraft.block.CloudPlatformBlock;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -56,6 +58,12 @@ public final class ModBlocks {
|
||||
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE = BLOCKS.registerBlock("star_fissure",
|
||||
StarFissureBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL)
|
||||
.lightLevel(state -> 6));
|
||||
public static final DeferredBlock<DecayBlock> DECAY = BLOCKS.registerBlock("decay",
|
||||
DecayBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.STONE)
|
||||
.strength(5.0F, 10.0F).noLootTable());
|
||||
public static final DeferredBlock<CloudPlatformBlock> CLOUD_PLATFORM = BLOCKS.registerBlock("cloud_platform",
|
||||
CloudPlatformBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.WHITE_STAINED_GLASS)
|
||||
.strength(1.5F).noOcclusion().noLootTable());
|
||||
|
||||
private ModBlocks() {
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** A bounded, position-preserving collection of Page stacks stored on a Notebook item. */
|
||||
public record NotebookContents(List<ItemStack> pages) {
|
||||
public static final int MAX_PAGES = 27;
|
||||
// Legacy did not impose a book-page cap. Our fixed-slot binder persists slot IDs
|
||||
// as unsigned bytes; 252 page positions (after its three input slots) fit safely.
|
||||
public static final int MAX_PAGES = 252;
|
||||
public static final NotebookContents EMPTY = new NotebookContents(List.of());
|
||||
public static final Codec<NotebookContents> CODEC = ItemStack.OPTIONAL_CODEC.listOf()
|
||||
.validate(NotebookContents::validate)
|
||||
|
||||
@@ -14,7 +14,7 @@ import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** Server-owned interpretation of ordered writing before Age-plan compilation. */
|
||||
public final class WritingGrammar {
|
||||
// A newly assembled book has one Link Panel plus at most 27 page positions.
|
||||
// A newly assembled book has one Link Panel plus the binder's page positions.
|
||||
public static final int MAX_NEW_ENTRIES = NotebookContents.MAX_PAGES + 1;
|
||||
private static final Set<String> MATERIAL_FEATURES = Set.of("rocks", "surface_lakes", "deep_lakes",
|
||||
"lava_lakes", "floating_islands", "tendrils", "spheres", "spikes", "obelisks");
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"variants":{"":{"model":"rivencraft:block/cloud_platform"}}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"color=blue": { "model": "rivencraft:block/decay_blue" },
|
||||
"color=purple": { "model": "rivencraft:block/decay_purple" },
|
||||
"color=red": { "model": "rivencraft:block/decay_red" },
|
||||
"color=white": { "model": "rivencraft:block/decay_white" }
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@
|
||||
"block.rivencraft.book_binder": "Book Binder",
|
||||
"container.rivencraft.book_binder": "Book Binder",
|
||||
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"block.rivencraft.decay": "Decay",
|
||||
"block.rivencraft.cloud_platform": "Cloud Platform",
|
||||
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||
"screen.rivencraft.ink_mixer.additive_hint": "Click the basin with an additive; right-click adds one.",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:block/cube_all","render_type":"minecraft:translucent","textures":{"all":"rivencraft:block/cloud_platform"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:block/cube_all","textures":{"all":"rivencraft:block/decay_blue"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:block/cube_all","textures":{"all":"rivencraft:block/decay_purple"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:block/cube_all","textures":{"all":"rivencraft:block/decay_red"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:block/cube_all","textures":{"all":"rivencraft:block/decay_white"}}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 868 B |
Binary file not shown.
|
After Width: | Height: | Size: 270 B |
Binary file not shown.
|
After Width: | Height: | Size: 271 B |
Binary file not shown.
|
After Width: | Height: | Size: 272 B |
Binary file not shown.
|
After Width: | Height: | Size: 237 B |
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"minecraft:bedrock",
|
||||
"minecraft:barrier",
|
||||
"minecraft:end_portal",
|
||||
"minecraft:end_portal_frame",
|
||||
"minecraft:end_gateway",
|
||||
"minecraft:nether_portal",
|
||||
"minecraft:reinforced_deepslate",
|
||||
"minecraft:structure_block",
|
||||
"minecraft:jigsaw",
|
||||
"minecraft:command_block",
|
||||
"minecraft:chain_command_block",
|
||||
"minecraft:repeating_command_block",
|
||||
"minecraft:light",
|
||||
"rivencraft:book_stand",
|
||||
"rivencraft:writing_desk",
|
||||
"rivencraft:book_binder",
|
||||
"rivencraft:ink_mixer",
|
||||
"rivencraft:star_fissure",
|
||||
"rivencraft:cloud_platform"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user