Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b2605b277 | ||
|
|
2f1e6bd6f2 | ||
|
|
9d5ff2a2cb |
No files matched your search
@@ -124,14 +124,20 @@ the top or underside of one of its blocks. Insert a bound Linking or Descriptive
|
||||
Book to activate it; the book remains visible on the receptacle. A Linking Book aimed at its
|
||||
own dimension still opens a visual portal, but travel requires Intra Linking.
|
||||
|
||||
Sky writing includes eight color pages and a Static Sky page. Multiple sky-color
|
||||
pages blend their colors, tinting the Age sky, book preview, and the fallback
|
||||
surface of portals linked there. Multiple celestial-color pages blend for the
|
||||
next written sun or moon. An Age may have no suns or moons at all;
|
||||
Static Sky animates the sky with television-like noise. Sun and Moon pages add
|
||||
Sky writing has shared Red, Green, Blue, White, and Black pages plus 25 D'ni
|
||||
number pages (0–24). Put colors before a Sky, Sun, or Moon page to color that
|
||||
target; trailing colors tint the sky. A number immediately before a color sets
|
||||
its strength to number × 10. Without a number, a color gets a seeded random
|
||||
strength from 0–24. RGB contributions add in any order, White brightens all
|
||||
channels, Black subtracts from all channels, and omitted channels remain zero.
|
||||
The resulting sky color also tints book previews and fallback portal surfaces.
|
||||
An Age may have no suns or moons at all; Static Sky animates with fine-grained
|
||||
television-like noise. Sun and Moon pages add
|
||||
independent bodies; preceding color, size, angle, period, phase, and direction
|
||||
pages configure the next body. The third and each subsequent sun add increasing
|
||||
instability. Newly generated page-free Ages also choose a deterministic sky,
|
||||
instability. Overlapping moon and sun discs produce an eclipse: the moon darkens,
|
||||
the sun's color brightens at close alignment, and world and sky lighting dim.
|
||||
Newly generated page-free Ages also choose a deterministic sky,
|
||||
color, and celestial arrangement from their Age seed; older saved Ages retain
|
||||
their previous sky.
|
||||
|
||||
@@ -227,6 +233,9 @@ Data-pack runtime profiles are documented in
|
||||
- NeoForge 21.1.264 or newer in the 21.1 line
|
||||
- Dynamic Dimensions AR Edition 0.9.1 and Bad Packets 0.8.2 (client and server)
|
||||
|
||||
Ruins is optional. When installed, its configured structures have individual
|
||||
authorable pages; see [Ruins compatibility](docs/RUINS_COMPATIBILITY.md).
|
||||
|
||||
Create is optional. When installed, Rivencraft uses its Ponder renderer for
|
||||
book previews when Immersive Portals is absent. Immersive Portals is also
|
||||
optional and takes priority in the default `AUTO` mode, providing a live
|
||||
|
||||
@@ -245,6 +245,11 @@ dependencies {
|
||||
// Keep them on the compile classpath without forcing either mod at runtime.
|
||||
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
||||
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
||||
// Ruins is optional; its pre-spawn event is only registered when Ruins is loaded.
|
||||
compileOnly 'curse.maven:ruins-227873:6391825'
|
||||
if (providers.gradleProperty('withRuins').map { it.toBoolean() }.orElse(false).get()) {
|
||||
localRuntime 'curse.maven:ruins-227873:6391825'
|
||||
}
|
||||
// TerraBlender's region API is only used by the guarded compat adapter.
|
||||
compileOnly "com.github.glitchfiend:TerraBlender-neoforge:${minecraft_version}-${terrablender_version}"
|
||||
if (providers.gradleProperty('withTerraBlender').map { it.toBoolean() }.orElse(false).get()) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,66 @@
|
||||
# Optional Ruins template pages (issue #91)
|
||||
|
||||
Rivencraft supports Ruins 1.21.1.2NF as an optional mod. When it is installed,
|
||||
every `.tml` template in Ruins' active `config/ruins_config/<generic-or-biome>/`
|
||||
folders receives a separate Ruins page in Rivencraft's creative and JEI page
|
||||
feeds. New templates added to Ruins' config appear after a restart; removing a
|
||||
template makes its page unavailable to new books. With Ruins absent, no Ruins
|
||||
pages or structure integration are active.
|
||||
|
||||
An authored Age with Ruins pages is admitted through Ruins' dimension gate.
|
||||
Rivencraft also admits the authored templates through their individual
|
||||
`dimensionsToSpawnIn` gates, which usually list only `overworld` and would
|
||||
otherwise prevent them from loading in an Age. Ruins still owns chunk
|
||||
inspection, spawn chance, surface validation, biome pools, and spacing.
|
||||
Rivencraft filters Ruins' weighted
|
||||
template choice to the exact pages in that Age and vetoes an unauthorized
|
||||
template in Ruins' pre-spawn event. A page therefore selects *which* template
|
||||
may appear; it does not guarantee a structure in every chunk. Manual
|
||||
`/testruin` placements are unaffected.
|
||||
|
||||
Repeating a template page multiplies that template's Ruins selection weight by
|
||||
its page count and adds one spawn attempt per chunk for each duplicate page.
|
||||
The extra attempts are capped at 32 per chunk (or Ruins' configured count if
|
||||
higher). Distinct template pages do not add attempts, and Ruins' spacing and
|
||||
surface checks still apply. As with Rivencraft's standard
|
||||
structure pages, the first three copies of each template are instability-free;
|
||||
each additional copy adds 100 instability. Ruins retains its own biome and
|
||||
surface checks, so the standard structure forced-biome penalty does not apply.
|
||||
|
||||
Ruins' event identifies a template only by its filename. If two active folders
|
||||
contain the same `.tml` basename, Rivencraft cannot distinguish them and
|
||||
disables all Ruins pages for that configuration. Give each template a unique
|
||||
filename to restore support. Ruins must also be able to parse the template and
|
||||
find a suitable surface in the selected biome; a page cannot repair an invalid
|
||||
or biome-incompatible template.
|
||||
|
||||
The adapter and dimension/selection mixin are version-sensitive to Ruins
|
||||
1.21.1.2NF. The published Rivencraft jar does not include Ruins. To include it
|
||||
in a development run, use `-PwithRuins=true`.
|
||||
|
||||
## Manual check
|
||||
|
||||
1. Start a client with Ruins 1.21.1.2NF and Rivencraft 0.6.9.3. Confirm the
|
||||
Ruins Pages tab contains one page per configured `.tml` file, and search a
|
||||
distinctive template name in JEI.
|
||||
2. Bind a new Descriptive Book with a Link Panel, Standard Terrain, a suitable
|
||||
surface biome, and one Ruins page (for example ZombieHut). Enter the Age and
|
||||
explore *unvisited* chunks. Ruins marks inspected chunks and will not retry
|
||||
chunks explored under the earlier broken build. Only the selected Ruins
|
||||
template should appear. Ruins'
|
||||
own chance and distance settings still determine how far exploration takes.
|
||||
3. For duplicate weighting, bind an Age with one copy of one template page and
|
||||
three copies of a second template page valid in the same biome. Across enough
|
||||
new candidate chunks, the second should be selected more often relative to
|
||||
the first, and there should be two additional spawn attempts per inspected
|
||||
chunk. Four copies of one template should add 100 instability, visible
|
||||
through the OP Age instability diagnostic, while three copies add none.
|
||||
4. Make another Age without a Ruins page and confirm Ruins structures do not
|
||||
appear there. Restart the game and confirm the authored Age keeps its page
|
||||
selection. Then remove Ruins and confirm Rivencraft starts normally without
|
||||
a Ruins tab.
|
||||
|
||||
The Ruins-enabled GameTest server passed all 90 required tests and loaded the
|
||||
template-dimension and attempt mixins. Its test world was removed afterward.
|
||||
Use the client check above to verify actual Ruins generation and the increased
|
||||
attempt rate.
|
||||
+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.6.8.7
|
||||
mod_version=0.6.9.3
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=pHEgQQUE
|
||||
|
||||
@@ -20,6 +20,7 @@ import net.dnorberg.rivencraft.network.ModNetworking;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
|
||||
import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility;
|
||||
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.book.AgeWeatherService;
|
||||
import net.dnorberg.rivencraft.book.AgeInstabilityRuntime;
|
||||
@@ -43,6 +44,7 @@ public final class Rivencraft {
|
||||
|
||||
public Rivencraft(IEventBus modEventBus, ModContainer modContainer) {
|
||||
AgeDimensionService.register();
|
||||
RuinsCompatibility.register();
|
||||
AgeStatusEffectProviders.register();
|
||||
AgeDecayProviders.register();
|
||||
AgeHazardProviders.register();
|
||||
@@ -82,6 +84,8 @@ public final class Rivencraft {
|
||||
NeoForge.EVENT_BUS.addListener(AgeInstabilityRuntime::onServerStopping);
|
||||
NeoForge.EVENT_BUS.addListener(AgeEnvironmentalProviders::onServerStopping);
|
||||
NeoForge.EVENT_BUS.addListener(AgeStatusEffectProviders::onServerStopping);
|
||||
NeoForge.EVENT_BUS.addListener((net.neoforged.neoforge.event.server.ServerStoppingEvent event) ->
|
||||
RuinsCompatibility.onServerStopping());
|
||||
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(LibraryLocateService::onServerTickPost);
|
||||
NeoForge.EVENT_BUS.addListener(LibraryLocateService::onServerStopping);
|
||||
|
||||
@@ -36,6 +36,7 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
@@ -44,10 +45,11 @@ import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
public final class BookReceptacleBlock extends BaseEntityBlock {
|
||||
public static final MapCodec<BookReceptacleBlock> CODEC = simpleCodec(BookReceptacleBlock::new);
|
||||
public static final DirectionProperty FACING = BlockStateProperties.FACING;
|
||||
public static final BooleanProperty HAS_BOOK = BlockStateProperties.HAS_BOOK;
|
||||
|
||||
public BookReceptacleBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(HAS_BOOK, false));
|
||||
}
|
||||
|
||||
@Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
|
||||
@@ -64,7 +66,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
|
||||
};
|
||||
}
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(FACING);
|
||||
builder.add(FACING, HAS_BOOK);
|
||||
}
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new BookReceptacleBlockEntity(pos, state);
|
||||
@@ -153,7 +155,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
|
||||
if (!state.is(replacement.getBlock()) && level instanceof ServerLevel server) {
|
||||
CrystalPortalNetwork.shutdown(server, pos);
|
||||
if (level.getBlockEntity(pos) instanceof BookReceptacleBlockEntity holder) {
|
||||
ItemStack book = holder.takeBook();
|
||||
ItemStack book = holder.takeBookOnBreak();
|
||||
if (!book.isEmpty()) level.addFreshEntity(new ItemEntity(level, pos.getX()+0.5, pos.getY()+0.5,
|
||||
pos.getZ()+0.5, book));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.AgeEnvironmentCompiler;
|
||||
import net.dnorberg.rivencraft.block.BookReceptacleBlock;
|
||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -52,18 +54,36 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
public ItemStack takeBook() {
|
||||
return removeBook(true);
|
||||
}
|
||||
|
||||
/** The block is already being replaced, so do not write its state back into the world. */
|
||||
public ItemStack takeBookOnBreak() {
|
||||
return removeBook(false);
|
||||
}
|
||||
|
||||
private ItemStack removeBook(boolean notify) {
|
||||
ItemStack result = book;
|
||||
book = ItemStack.EMPTY;
|
||||
destination = null;
|
||||
skyColor = 0xFFFFFF;
|
||||
sync();
|
||||
if (notify) sync();
|
||||
else setChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void sync() {
|
||||
setChanged();
|
||||
if (level != null && !level.isClientSide)
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 2);
|
||||
if (level instanceof ServerLevel server) {
|
||||
BlockState state = server.getBlockState(worldPosition);
|
||||
if (!state.is(ModBlocks.BOOK_RECEPTACLE.get())) return;
|
||||
boolean hasBook = !book.isEmpty();
|
||||
if (state.getValue(BookReceptacleBlock.HAS_BOOK) != hasBook) {
|
||||
state = state.setValue(BookReceptacleBlock.HAS_BOOK, hasBook);
|
||||
server.setBlock(worldPosition, state, 2);
|
||||
}
|
||||
server.sendBlockUpdated(worldPosition, state, state, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,6 +118,10 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
||||
public static void serverTick(net.minecraft.world.level.Level world, BlockPos pos, BlockState state,
|
||||
BookReceptacleBlockEntity holder) {
|
||||
if (!(world instanceof ServerLevel level)) return;
|
||||
// Reconcile receptacles saved before HAS_BOOK existed, including an
|
||||
// occupied receptacle restored from an older pre-release world.
|
||||
boolean hasBook = !holder.book.isEmpty();
|
||||
if (state.getValue(BookReceptacleBlock.HAS_BOOK) != hasBook) holder.sync();
|
||||
if (holder.book.isEmpty() || (!holder.restoreView && level.getGameTime() % 200 != 0)) return;
|
||||
// Startup maintenance must never turn a saved portal off while its
|
||||
// chunks and block entities are still arriving. Structural edits are
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.RuinsCompatibility;
|
||||
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||
@@ -84,6 +85,7 @@ public final class AgeDimensionService {
|
||||
if (level != null) {
|
||||
applyCelestialClock(level, age);
|
||||
SereneSeasonsCompatibility.registerAge(server, age);
|
||||
RuinsCompatibility.registerAge(age);
|
||||
}
|
||||
}
|
||||
synchronizeImmersivePortals(server);
|
||||
@@ -141,6 +143,7 @@ public final class AgeDimensionService {
|
||||
if (existing != null) {
|
||||
applyCelestialClock(existing, definition);
|
||||
SereneSeasonsCompatibility.registerAge(server, definition);
|
||||
RuinsCompatibility.registerAge(definition);
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -157,6 +160,7 @@ public final class AgeDimensionService {
|
||||
} else {
|
||||
applyCelestialClock(loaded, definition);
|
||||
SereneSeasonsCompatibility.registerAge(server, definition);
|
||||
RuinsCompatibility.registerAge(definition);
|
||||
synchronizeImmersivePortals(server);
|
||||
}
|
||||
return loaded;
|
||||
|
||||
@@ -108,11 +108,7 @@ public final class AgeEnvironmentCompiler {
|
||||
|
||||
/** An authored color is fixed; -1 means the biome or selected vanilla sky supplies it. */
|
||||
public static int skyColor(AgeGenerationPlan plan) {
|
||||
if (plan == null) return -1;
|
||||
return AgeSkyProfile.mixColors(plan.activeSymbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY_COLOR)
|
||||
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
|
||||
.map(AgeSkyProfile::color).toList());
|
||||
return AgeSkyProfile.authoredSkyColor(plan);
|
||||
}
|
||||
|
||||
public static int selectedSkyColor(AgeGenerationPlan plan, ResourceLocation fallback, int biomeSkyColor) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
@@ -235,6 +236,9 @@ public final class AgeFeatureSelection {
|
||||
}
|
||||
|
||||
public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) {
|
||||
if (RuinsTemplatePages.isSymbol(choice))
|
||||
return RuinsTemplatePages.contains(choice) ? null : "Ruins template " + choice
|
||||
+ " is unavailable; restore Ruins and its template before loading this Age";
|
||||
if (AgeStructureSelection.feature(choice))
|
||||
return AgeStructureSelection.error(choice, profile, null, registries);
|
||||
if (choice.equals(AgeDenseOres.ID))
|
||||
|
||||
@@ -188,7 +188,7 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
String raw = item.getString("Value");
|
||||
SymbolSemantics.Value value = switch (role) {
|
||||
case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw));
|
||||
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER ->
|
||||
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER, COLOR_NUMBER ->
|
||||
new SymbolSemantics.NumberValue(Double.parseDouble(raw));
|
||||
default -> new SymbolSemantics.ChoiceValue(raw);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
@@ -150,9 +151,12 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
||||
|| !(symbol.semantics().value()
|
||||
instanceof net.dnorberg.rivencraft.writing.SymbolSemantics.ResourceValue resource)) continue;
|
||||
String name = COUNTED_STRUCTURES.get(resource.id());
|
||||
if (name == null && RuinsTemplatePages.isSymbol(resource.id()))
|
||||
name = RuinsTemplatePages.fallbackName(resource.id());
|
||||
if (name == null) continue;
|
||||
int count = structureCounts.merge(resource.id(), 1, Integer::sum);
|
||||
if (AgeStructureSelection.requiresBiomeOverride(resource.id(), plan.biomeSelection(), registries)) {
|
||||
if (COUNTED_STRUCTURES.containsKey(resource.id())
|
||||
&& AgeStructureSelection.requiresBiomeOverride(resource.id(), plan.biomeSelection(), registries)) {
|
||||
contributions.add(new Contribution("forced_biome_" + resource.id().getPath(), symbol.index(),
|
||||
FORCED_STRUCTURE_BIOME_COST,
|
||||
name + " occurrence " + count + " is forced into an unsupported biome"));
|
||||
@@ -168,7 +172,7 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
||||
|
||||
static int featureCost(ResourceLocation feature, int occurrence) {
|
||||
if (occurrence < 1) throw new IllegalArgumentException("Feature occurrence must be positive");
|
||||
return COUNTED_STRUCTURES.containsKey(feature) && occurrence > 3
|
||||
return (COUNTED_STRUCTURES.containsKey(feature) || RuinsTemplatePages.isSymbol(feature)) && occurrence > 3
|
||||
? EXCESS_LEGACY_STRUCTURE_COST : 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,9 +125,9 @@ public final class AgePersistenceGameTests {
|
||||
new AgeReference(UUID.randomUUID(), "Stable Writing"), stable);
|
||||
AgeDefinition defectiveAge = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Defective Writing"), defective);
|
||||
helper.assertValueEqual(stableAge.instability().score(), 0,
|
||||
helper.assertValueEqual(nonCelestialInstability(stableAge), 0,
|
||||
"Missing seeded branches do not add instability");
|
||||
helper.assertValueEqual(defectiveAge.instability().score(), 600,
|
||||
helper.assertValueEqual(nonCelestialInstability(defectiveAge), 600,
|
||||
"Out-of-order and repeated terrain pages each add their saved penalty");
|
||||
helper.assertTrue(defectiveAge.instability().breakdown().stream()
|
||||
.anyMatch(part -> part.reason().equals("duplicate") && part.pageIndex() == 3),
|
||||
@@ -184,9 +184,9 @@ public final class AgePersistenceGameTests {
|
||||
new AgeReference(UUID.randomUUID(), "Three " + structure), structureWriting(structure, 3));
|
||||
AgeDefinition four = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Four " + structure), structureWriting(structure, 4));
|
||||
helper.assertValueEqual(three.instability().score(), 0,
|
||||
helper.assertValueEqual(nonCelestialInstability(three), 0,
|
||||
"The first three " + structure + " pages retain Legacy's zero cost");
|
||||
helper.assertValueEqual(four.instability().score(), 100,
|
||||
helper.assertValueEqual(nonCelestialInstability(four), 100,
|
||||
"The fourth " + structure + " page must add Legacy's 100-point cost");
|
||||
helper.assertTrue(four.instability().breakdown().stream().anyMatch(part ->
|
||||
part.reason().equals("excess_" + structure) && part.pageIndex() == 8),
|
||||
@@ -207,6 +207,13 @@ public final class AgePersistenceGameTests {
|
||||
"Modern repeated structures must use the same deterministic scoring path");
|
||||
helper.assertValueEqual(AgeInstability.featureCost(ResourceLocation.parse("rivencraft:dungeons"), 4), 0,
|
||||
"A Legacy-neutral feature must remain neutral when repeated");
|
||||
ResourceLocation ruin = net.dnorberg.rivencraft.compat.RuinsTemplatePages.symbolId("ZombieHut.tml");
|
||||
helper.assertValueEqual(AgeInstability.featureCost(ruin, 1), 0,
|
||||
"The first Ruins template page is free like a standard structure");
|
||||
helper.assertValueEqual(AgeInstability.featureCost(ruin, 3), 0,
|
||||
"The first three copies of a Ruins page are free");
|
||||
helper.assertValueEqual(AgeInstability.featureCost(ruin, 4), 100,
|
||||
"Each Ruins page beyond the third adds 100 instability");
|
||||
|
||||
OrderedWriting duplicateBright = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
@@ -216,7 +223,7 @@ public final class AgePersistenceGameTests {
|
||||
));
|
||||
AgeDefinition bright = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Double Bright"), duplicateBright);
|
||||
helper.assertValueEqual(bright.instability().score(), 1500,
|
||||
helper.assertValueEqual(nonCelestialInstability(bright), 1500,
|
||||
"Each Bright Lighting page costs 500 and the extra controller costs another 500");
|
||||
helper.assertValueEqual(bright.instability().breakdown().stream()
|
||||
.filter(part -> part.reason().equals("bright_lighting")).count(), 2L,
|
||||
@@ -420,6 +427,13 @@ public final class AgePersistenceGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
/** Isolate the writing-cost assertion from seed-completed extra-sun instability. */
|
||||
private static int nonCelestialInstability(AgeDefinition age) {
|
||||
return age.instability().breakdown().stream()
|
||||
.filter(part -> !part.reason().equals("extra_sun"))
|
||||
.mapToInt(AgeInstability.Contribution::points).sum();
|
||||
}
|
||||
|
||||
private static CompoundTag entry(
|
||||
UUID id,
|
||||
String name,
|
||||
|
||||
@@ -48,7 +48,7 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
public static AgeSkyProfile compile(AgeGenerationPlan plan, AgeTraits traits) {
|
||||
if (plan == null) return new AgeSkyProfile(Style.INHERITED, -1, List.of());
|
||||
Style style = Style.INHERITED;
|
||||
int color = AgeEnvironmentCompiler.skyColor(plan);
|
||||
int color = authoredSkyColor(plan);
|
||||
boolean customBodies = hasCustomCelestials(plan);
|
||||
List<Body> bodies = new ArrayList<>();
|
||||
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
|
||||
@@ -58,18 +58,32 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
float phase = seeded.celestialPhase();
|
||||
float size = 1.0F;
|
||||
List<Integer> bodyColors = new ArrayList<>();
|
||||
ColorAccumulator pendingColors = new ColorAccumulator();
|
||||
Integer colorMultiplier = null;
|
||||
boolean clockwise = true;
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
if (!active.contains(symbol.index())) continue;
|
||||
SymbolSemantics semantics = symbol.semantics();
|
||||
if (semantics.role() != SymbolSemantics.Role.COLOR
|
||||
&& semantics.role() != SymbolSemantics.Role.COLOR_NUMBER) colorMultiplier = null;
|
||||
switch (semantics.role()) {
|
||||
case SKY -> style = switch (((SymbolSemantics.ChoiceValue) semantics.value()).id()) {
|
||||
case "overworld" -> Style.OVERWORLD;
|
||||
case "nether" -> Style.NETHER;
|
||||
case "end" -> Style.END;
|
||||
case "static" -> Style.STATIC;
|
||||
default -> throw new IllegalArgumentException("Unknown sky style");
|
||||
};
|
||||
case SKY -> {
|
||||
pendingColors.clear();
|
||||
style = switch (((SymbolSemantics.ChoiceValue) semantics.value()).id()) {
|
||||
case "overworld" -> Style.OVERWORLD;
|
||||
case "nether" -> Style.NETHER;
|
||||
case "end" -> Style.END;
|
||||
case "static" -> Style.STATIC;
|
||||
default -> throw new IllegalArgumentException("Unknown sky style");
|
||||
};
|
||||
}
|
||||
case COLOR_NUMBER -> colorMultiplier = (int) ((SymbolSemantics.NumberValue)
|
||||
semantics.value()).amount();
|
||||
case COLOR -> {
|
||||
pendingColors.add(((SymbolSemantics.ChoiceValue) semantics.value()).id(),
|
||||
colorMultiplier == null ? randomColorNumber(plan.ageSeed(), symbol.index()) : colorMultiplier);
|
||||
colorMultiplier = null;
|
||||
}
|
||||
case PERIOD_MODIFIER -> period = ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
case ANGLE_MODIFIER -> angle = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
case PHASE_MODIFIER -> phase = (float) (((SymbolSemantics.NumberValue) semantics.value()).amount() / 360.0D);
|
||||
@@ -80,7 +94,8 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
.id().equals("clockwise");
|
||||
case CELESTIAL -> {
|
||||
String choice = ((SymbolSemantics.ChoiceValue) semantics.value()).id();
|
||||
int bodyColor = bodyColors.isEmpty() ? 0xFFFFFF : mixColors(bodyColors);
|
||||
int bodyColor = pendingColors.written() ? pendingColors.rgb()
|
||||
: bodyColors.isEmpty() ? 0 : mixColors(bodyColors);
|
||||
if (choice.equals("normal_time")) {
|
||||
if (customBodies) {
|
||||
bodies.add(new Body(Kind.SUN, bodyColor, size, angle, period, phase, clockwise));
|
||||
@@ -96,6 +111,7 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
phase = seeded.celestialPhase();
|
||||
size = 1.0F;
|
||||
bodyColors.clear();
|
||||
pendingColors.clear();
|
||||
clockwise = true;
|
||||
}
|
||||
default -> { }
|
||||
@@ -124,6 +140,74 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
| (int) ((blue + count / 2) / count);
|
||||
}
|
||||
|
||||
/** Shared color pages affect the next Sky or celestial page; trailing colors tint the sky. */
|
||||
public static int authoredSkyColor(AgeGenerationPlan plan) {
|
||||
if (plan == null) return -1;
|
||||
ColorAccumulator pending = new ColorAccumulator();
|
||||
Integer multiplier = null;
|
||||
int skyColor = -1;
|
||||
List<Integer> legacyColors = new ArrayList<>();
|
||||
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
if (!active.contains(symbol.index())) continue;
|
||||
SymbolSemantics semantics = symbol.semantics();
|
||||
if (semantics.role() != SymbolSemantics.Role.COLOR
|
||||
&& semantics.role() != SymbolSemantics.Role.COLOR_NUMBER) multiplier = null;
|
||||
switch (semantics.role()) {
|
||||
case COLOR_NUMBER -> multiplier = (int) ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
case COLOR -> {
|
||||
pending.add(((SymbolSemantics.ChoiceValue) semantics.value()).id(),
|
||||
multiplier == null ? randomColorNumber(plan.ageSeed(), symbol.index()) : multiplier);
|
||||
multiplier = null;
|
||||
}
|
||||
case SKY -> {
|
||||
if (pending.written()) skyColor = pending.rgb();
|
||||
pending.clear();
|
||||
}
|
||||
case CELESTIAL -> pending.clear();
|
||||
case SKY_COLOR -> legacyColors.add(color(
|
||||
((SymbolSemantics.ChoiceValue) semantics.value()).id()));
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
if (pending.written()) skyColor = pending.rgb();
|
||||
return skyColor != -1 ? skyColor : mixColors(legacyColors);
|
||||
}
|
||||
|
||||
private static int randomColorNumber(long ageSeed, int pageIndex) {
|
||||
return new Random(ageSeed ^ (0x9E3779B97F4A7C15L * (pageIndex + 1L))).nextInt(25);
|
||||
}
|
||||
|
||||
private static final class ColorAccumulator {
|
||||
private int red;
|
||||
private int green;
|
||||
private int blue;
|
||||
private boolean written;
|
||||
|
||||
void add(String color, int multiplier) {
|
||||
int amount = multiplier * 10;
|
||||
written = true;
|
||||
switch (color) {
|
||||
case "red" -> red += amount;
|
||||
case "green" -> green += amount;
|
||||
case "blue" -> blue += amount;
|
||||
case "white" -> { red += amount; green += amount; blue += amount; }
|
||||
case "black" -> { red -= amount; green -= amount; blue -= amount; }
|
||||
default -> throw new IllegalArgumentException("Unknown shared color " + color);
|
||||
}
|
||||
}
|
||||
|
||||
boolean written() { return written; }
|
||||
|
||||
int rgb() {
|
||||
return clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
}
|
||||
|
||||
void clear() { red = green = blue = 0; written = false; }
|
||||
|
||||
private static int clamp(int channel) { return Math.max(0, Math.min(255, channel)); }
|
||||
}
|
||||
|
||||
private static int randomColor(Random random) {
|
||||
int count = 1 + random.nextInt(3);
|
||||
List<Integer> colors = new ArrayList<>(count);
|
||||
@@ -132,12 +216,30 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
}
|
||||
|
||||
public static boolean hasCustomCelestials(AgeGenerationPlan plan) {
|
||||
return plan != null && plan.activeSymbols().stream().anyMatch(symbol ->
|
||||
return plan != null && (hasColorForCelestial(plan) || plan.activeSymbols().stream().anyMatch(symbol ->
|
||||
symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL
|
||||
&& !((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("normal_time")
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL_COLOR
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.SIZE_MODIFIER
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.DIRECTION_MODIFIER);
|
||||
|| symbol.semantics().role() == SymbolSemantics.Role.DIRECTION_MODIFIER));
|
||||
}
|
||||
|
||||
private static boolean hasColorForCelestial(AgeGenerationPlan plan) {
|
||||
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
|
||||
boolean pending = false;
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
if (!active.contains(symbol.index())) continue;
|
||||
switch (symbol.semantics().role()) {
|
||||
case COLOR -> pending = true;
|
||||
case SKY -> pending = false;
|
||||
case CELESTIAL -> {
|
||||
if (pending) return true;
|
||||
pending = false;
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean custom() {
|
||||
@@ -154,12 +256,53 @@ public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||
/ (24000.0D * body.period()) + body.phase();
|
||||
float altitude = (float) Math.cos(orbit * Math.PI * 2.0D);
|
||||
if (altitude <= 0.0F) continue;
|
||||
if (body.kind() == Kind.SUN) sunlight += altitude;
|
||||
if (body.kind() == Kind.SUN) sunlight += altitude * (1.0F - 0.85F * eclipseFor(body, dayTime));
|
||||
else moonlight += altitude * 0.18F;
|
||||
}
|
||||
return Math.min(1.0F, sunlight > 0.0F ? sunlight : moonlight);
|
||||
}
|
||||
|
||||
/** Disc overlap in the rendered sky, from first contact (0) to centered alignment (1). */
|
||||
public float eclipseFor(Body body, double dayTime) {
|
||||
float strongest = 0.0F;
|
||||
net.minecraft.world.phys.Vec3 bodyDirection = direction(body, dayTime);
|
||||
for (Body other : bodies) {
|
||||
if (other.kind() == body.kind()) continue;
|
||||
double dot = Math.max(-1.0D, Math.min(1.0D,
|
||||
bodyDirection.dot(direction(other, dayTime))));
|
||||
double separation = Math.acos(dot);
|
||||
// The outer 32x32 quad is only glow. The opaque body occupies its
|
||||
// central 8x8 pixels, so contact must use one quarter of its size.
|
||||
double radius = Math.atan((body.kind() == Kind.SUN ? 30.0D : 20.0D)
|
||||
* body.size() * 0.25D / 100.0D)
|
||||
+ Math.atan((other.kind() == Kind.SUN ? 30.0D : 20.0D)
|
||||
* other.size() * 0.25D / 100.0D);
|
||||
if (radius > 0.0D) strongest = Math.max(strongest,
|
||||
(float) Math.max(0.0D, 1.0D - separation / radius));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
|
||||
public float eclipseAt(double dayTime) {
|
||||
float strongest = 0.0F;
|
||||
for (Body body : bodies) {
|
||||
if (body.kind() == Kind.SUN && direction(body, dayTime).y > 0.0D)
|
||||
strongest = Math.max(strongest, eclipseFor(body, dayTime));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
|
||||
private static net.minecraft.world.phys.Vec3 direction(Body body, double dayTime) {
|
||||
double orbit = body.period() == 0.0D ? body.phase()
|
||||
: (body.clockwise() ? 1.0D : -1.0D) * dayTime
|
||||
/ (24000.0D * body.period()) + body.phase();
|
||||
double tilt = orbit * Math.PI * 2.0D;
|
||||
double bearing = Math.toRadians(body.angle());
|
||||
double lateral = Math.sin(tilt);
|
||||
return new net.minecraft.world.phys.Vec3(
|
||||
Math.sin(bearing) * lateral, Math.cos(tilt), Math.cos(bearing) * lateral);
|
||||
}
|
||||
|
||||
public static int color(String name) {
|
||||
return switch (name) {
|
||||
case "red" -> COLORS[0];
|
||||
|
||||
@@ -52,10 +52,11 @@ public final class EnvironmentWritingGameTests {
|
||||
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
|
||||
Set<String> expected = Set.of(
|
||||
"lighting/normal", "lighting/dark", "lighting/bright",
|
||||
"sky/overworld", "sky/nether", "sky/end", "sky/static", "sky_color/purple",
|
||||
"sky/overworld", "sky/nether", "sky/end", "sky/static", "color/red", "color/green",
|
||||
"color/blue", "color/black", "color/white", "number/0", "number/24",
|
||||
"weather/normal", "weather/fast", "weather/slow", "weather/off",
|
||||
"weather/always", "weather/storm", "celestial/normal_time", "celestial/sun",
|
||||
"celestial/moon", "celestial_color/red", "size/large", "angle/east",
|
||||
"celestial/moon", "size/large", "angle/east",
|
||||
"direction/clockwise", "direction/counterclockwise",
|
||||
"season/no_seasons", "season/seasons", "season/eternal_winter",
|
||||
"season/eternal_summer", "season/eternal_fall", "season/eternal_spring",
|
||||
@@ -69,6 +70,9 @@ public final class EnvironmentWritingGameTests {
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(expected)
|
||||
&& creative.containsAll(expected),
|
||||
"Every supported environment page must enter the creative index JEI also consumes");
|
||||
helper.assertValueEqual(PageItem.bundledCreativeSymbols().stream()
|
||||
.filter(path -> path.startsWith("number/")).count(), 25L,
|
||||
"Creative and JEI must expose all 25 D'ni number pages");
|
||||
for (String path : expected) {
|
||||
var id = ResourceLocation.fromNamespaceAndPath("rivencraft", path);
|
||||
helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(id))
|
||||
@@ -140,8 +144,8 @@ public final class EnvironmentWritingGameTests {
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredStaticSkyColorSurvivesWritingAndFeedsPreview(GameTestHelper helper) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/static"),
|
||||
page("sky_color/purple"), page("celestial/normal_time")));
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("number/20"),
|
||||
page("color/blue"), page("sky/static"), page("celestial/normal_time")));
|
||||
AgeTraits seeded = AgeTraits.select(44L);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 44L, AgeGenerationProfiles.STANDARD,
|
||||
seeded, helper.getLevel().registryAccess());
|
||||
@@ -152,15 +156,15 @@ public final class EnvironmentWritingGameTests {
|
||||
"The static sky must select Rivencraft's custom renderer");
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(plan,
|
||||
BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF),
|
||||
0x9870B9, "The authored color must tint the sky and preview consistently");
|
||||
0x0000C8, "The authored color must tint the sky and preview consistently");
|
||||
AgeGenerationPlan restored = AgeGenerationPlan.load(plan.save());
|
||||
helper.assertTrue(AgeEnvironmentCompiler.staticSky(restored),
|
||||
"A saved Age must retain its static sky choice");
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(restored), 0x9870B9,
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(restored), 0x0000C8,
|
||||
"A saved Age must retain its sky color choice");
|
||||
AgeGenerationPlan coloredEnd = AgeGenerationPlan.compile(new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"),
|
||||
page("sky_color/red"), page("celestial/normal_time"))),
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("number/15"),
|
||||
page("color/red"), page("sky/end"), page("celestial/normal_time"))),
|
||||
45L, AgeGenerationProfiles.STANDARD, AgeTraits.select(45L),
|
||||
helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(coloredEnd,
|
||||
@@ -169,9 +173,10 @@ public final class EnvironmentWritingGameTests {
|
||||
"A color page must make non-Overworld skies renderable by the custom sky effect");
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(coloredEnd,
|
||||
BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF),
|
||||
0xB74A50, "A color page must override the selected sky type's default tint");
|
||||
0x960000, "A color page must override the selected sky type's default tint");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("sky/static")
|
||||
&& PageItem.bundledCreativeSymbols().contains("sky_color/purple"),
|
||||
&& PageItem.bundledCreativeSymbols().contains("color/blue")
|
||||
&& PageItem.bundledCreativeSymbols().contains("number/24"),
|
||||
"New sky pages must be available through the Creative and JEI page catalog");
|
||||
helper.succeed();
|
||||
}
|
||||
@@ -179,18 +184,17 @@ public final class EnvironmentWritingGameTests {
|
||||
@GameTest(template = "empty")
|
||||
public static void skyAndCelestialColorsBlendWithoutRequiringBodies(GameTestHelper helper) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/overworld"),
|
||||
page("sky_color/red"), page("sky_color/blue"),
|
||||
page("celestial_color/yellow"), page("celestial_color/blue"), page("celestial/sun")));
|
||||
WritingEntry.linkPanel(), page("terrain/standard"),
|
||||
page("number/10"), page("color/red"), page("number/5"), page("color/blue"),
|
||||
page("sky/overworld"), page("number/12"), page("color/green"),
|
||||
page("number/7"), page("color/blue"), page("celestial/sun")));
|
||||
helper.assertTrue(WritingGrammar.parse(writing).canCreate(),
|
||||
"Two sky colors and two colors for one sun must be valid writing");
|
||||
AgeTraits traits = AgeTraits.select(46L);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 46L,
|
||||
AgeGenerationProfiles.STANDARD, traits, helper.getLevel().registryAccess());
|
||||
int skyColor = AgeSkyProfile.mixColors(List.of(
|
||||
AgeSkyProfile.color("red"), AgeSkyProfile.color("blue")));
|
||||
int sunColor = AgeSkyProfile.mixColors(List.of(
|
||||
AgeSkyProfile.color("yellow"), AgeSkyProfile.color("blue")));
|
||||
int skyColor = 0x640032;
|
||||
int sunColor = 0x007846;
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), skyColor,
|
||||
"Sky color pages must blend without discarding either page");
|
||||
AgeSkyProfile profile = AgeSkyProfile.compile(plan, traits);
|
||||
@@ -206,6 +210,58 @@ public final class EnvironmentWritingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void colorNumbersUseDniDigitsAndMissingChannelsRemainZero(GameTestHelper helper) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("number/12"), page("color/red"),
|
||||
page("number/5"), page("color/white"), page("number/3"), page("color/black"),
|
||||
page("sky/overworld")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 49L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(49L), helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), 0x8C1414,
|
||||
"White adds and Black subtracts from every channel; missing green and blue start at zero");
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(AgeGenerationPlan.load(plan.save())), 0x8C1414,
|
||||
"D'ni color multipliers must remain frozen in a saved Age plan");
|
||||
|
||||
OrderedWriting unnumbered = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("color/green"), page("sky/overworld")));
|
||||
AgeGenerationPlan randomPlan = AgeGenerationPlan.compile(unnumbered, 50L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(50L), helper.getLevel().registryAccess());
|
||||
int color = AgeEnvironmentCompiler.skyColor(randomPlan);
|
||||
helper.assertTrue((color & 0xFF00FF) == 0 && ((color >> 8) & 255) <= 240
|
||||
&& ((color >> 8) & 255) % 10 == 0,
|
||||
"An unnumbered color gets one seeded base-25 digit and leaves omitted channels at zero");
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(AgeGenerationPlan.load(randomPlan.save())), color,
|
||||
"A random color digit must not change when an Age is reloaded");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void alignedMoonEclipsesSunAndDimsLight(GameTestHelper helper) {
|
||||
AgeSkyProfile.Body sun = new AgeSkyProfile.Body(AgeSkyProfile.Kind.SUN,
|
||||
0xCC8844, 1.0F, 0.0F, 1.0D, 0.0F, true);
|
||||
AgeSkyProfile.Body moon = new AgeSkyProfile.Body(AgeSkyProfile.Kind.MOON,
|
||||
0xFFFFFF, 1.0F, 0.0F, 1.0D, 0.0F, true);
|
||||
AgeSkyProfile clear = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA, List.of(sun));
|
||||
AgeSkyProfile eclipse = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA,
|
||||
List.of(sun, moon));
|
||||
helper.assertValueEqual(eclipse.eclipseAt(0.0D), 1.0F,
|
||||
"Centered sun and moon must produce a full eclipse");
|
||||
helper.assertTrue(eclipse.lightAt(0.0D) < clear.lightAt(0.0D),
|
||||
"An eclipse must dim world skylight, not just the rendered sprites");
|
||||
AgeSkyProfile separated = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA,
|
||||
List.of(sun, new AgeSkyProfile.Body(AgeSkyProfile.Kind.MOON,
|
||||
0xFFFFFF, 1.0F, 0.0F, 1.0D, 0.5F, true)));
|
||||
helper.assertValueEqual(separated.eclipseAt(0.0D), 0.0F,
|
||||
"A moon on the far side of the sky must not eclipse the sun");
|
||||
AgeSkyProfile glowOnlyOverlap = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, 0x6688AA,
|
||||
List.of(sun, new AgeSkyProfile.Body(AgeSkyProfile.Kind.MOON,
|
||||
0xFFFFFF, 1.0F, 0.0F, 1.0D, 0.04F, true)));
|
||||
helper.assertValueEqual(glowOnlyOverlap.eclipseAt(0.0D), 0.0F,
|
||||
"Overlapping glows must not count as an eclipse before the opaque discs touch");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void randomAndAuthoredCelestialsKeepIndependentOrbits(GameTestHelper helper) {
|
||||
AgeTraits traits = AgeTraits.select(12345L);
|
||||
@@ -217,7 +273,7 @@ public final class EnvironmentWritingGameTests {
|
||||
"Random Ages must obey the same celestial limits as authored Ages");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"),
|
||||
page("angle/east"), page("direction/counterclockwise"), page("celestial_color/red"),
|
||||
page("angle/east"), page("direction/counterclockwise"), page("number/24"), page("color/red"),
|
||||
page("size/large"), page("period/half"), page("celestial/sun"),
|
||||
page("direction/clockwise"), page("celestial/moon")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 12345L, AgeGenerationProfiles.STANDARD,
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Axis;
|
||||
import net.dnorberg.rivencraft.block.BookReceptacleBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.client.model.BookModel;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
@@ -30,11 +31,15 @@ public final class BookReceptacleRenderer implements BlockEntityRenderer<BookRec
|
||||
@Override
|
||||
public void render(BookReceptacleBlockEntity receptacle, float partialTick, PoseStack poseStack,
|
||||
MultiBufferSource buffers, int packedLight, int packedOverlay) {
|
||||
if (receptacle.getBook().isEmpty()) return;
|
||||
if (receptacle.getLevel() == null) return;
|
||||
var state = receptacle.getLevel().getBlockState(receptacle.getBlockPos());
|
||||
if (!state.is(ModBlocks.BOOK_RECEPTACLE.get())
|
||||
|| !state.getValue(BookReceptacleBlock.HAS_BOOK)
|
||||
|| receptacle.getBook().isEmpty()) return;
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5, 0.5, 0.5);
|
||||
Direction facing = receptacle.getBlockState().getValue(BookReceptacleBlock.FACING);
|
||||
Direction facing = state.getValue(BookReceptacleBlock.FACING);
|
||||
switch (facing) {
|
||||
case UP -> {
|
||||
// Rotate the whole assembled top-mounted receptacle/book pair
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
@@ -22,6 +23,7 @@ import net.minecraft.resources.ResourceLocation;
|
||||
/** An authored sky can use vanilla celestial rendering or Legacy-style moving TV static. */
|
||||
public final class RivencraftSkyEffects extends DimensionSpecialEffects {
|
||||
private static final int GRID = 16;
|
||||
private static final int STATIC_GRID = 48;
|
||||
private static final float RADIUS = 100.0F;
|
||||
private static final ResourceLocation SUN = ResourceLocation.withDefaultNamespace("textures/environment/sun.png");
|
||||
private static final ResourceLocation MOON = ResourceLocation.withDefaultNamespace("textures/environment/moon_phases.png");
|
||||
@@ -62,6 +64,8 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
|
||||
case STATIC -> 0x929292;
|
||||
default -> level.getBiome(camera.getBlockPosition()).value().getSkyColor();
|
||||
};
|
||||
double time = level.getDayTime() + partialTick;
|
||||
float eclipseShade = 1.0F - 0.65F * sky.eclipseAt(time);
|
||||
|
||||
RenderSystem.depthMask(false);
|
||||
RenderSystem.disableCull();
|
||||
@@ -70,20 +74,21 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
|
||||
FogRenderer.setupNoFog();
|
||||
BufferBuilder vertices = Tesselator.getInstance().begin(
|
||||
VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
||||
int grid = sky.style() == AgeSkyProfile.Style.STATIC ? STATIC_GRID : GRID;
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
for (int sign : new int[]{-1, 1}) {
|
||||
for (int x = 0; x < GRID; x++) {
|
||||
for (int y = 0; y < GRID; y++) {
|
||||
for (int x = 0; x < grid; x++) {
|
||||
for (int y = 0; y < grid; y++) {
|
||||
int noise = sky.style() == AgeSkyProfile.Style.STATIC
|
||||
? 65 + random.nextInt(160) : 255;
|
||||
int red = ((tint >> 16) & 255) * noise / 255;
|
||||
int green = ((tint >> 8) & 255) * noise / 255;
|
||||
int blue = (tint & 255) * noise / 255;
|
||||
int red = (int) (((tint >> 16) & 255) * noise / 255.0F * eclipseShade);
|
||||
int green = (int) (((tint >> 8) & 255) * noise / 255.0F * eclipseShade);
|
||||
int blue = (int) ((tint & 255) * noise / 255.0F * eclipseShade);
|
||||
int color = 0xFF000000 | red << 16 | green << 8 | blue;
|
||||
float left = -RADIUS + x * (2 * RADIUS / GRID);
|
||||
float right = -RADIUS + (x + 1) * (2 * RADIUS / GRID);
|
||||
float bottom = -RADIUS + y * (2 * RADIUS / GRID);
|
||||
float top = -RADIUS + (y + 1) * (2 * RADIUS / GRID);
|
||||
float left = -RADIUS + x * (2 * RADIUS / grid);
|
||||
float right = -RADIUS + (x + 1) * (2 * RADIUS / grid);
|
||||
float bottom = -RADIUS + y * (2 * RADIUS / grid);
|
||||
float top = -RADIUS + (y + 1) * (2 * RADIUS / grid);
|
||||
vertex(vertices, matrix, axis, sign, left, bottom, color);
|
||||
vertex(vertices, matrix, axis, sign, right, bottom, color);
|
||||
vertex(vertices, matrix, axis, sign, right, top, color);
|
||||
@@ -96,8 +101,11 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
|
||||
if (!sky.bodies().isEmpty()) {
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.defaultBlendFunc();
|
||||
double time = level.getGameTime() + partialTick;
|
||||
for (AgeSkyProfile.Body body : sky.bodies()) drawBody(pose, body, time);
|
||||
// Draw suns first so an aligned moon can pass visibly in front of them.
|
||||
for (AgeSkyProfile.Body body : sky.bodies())
|
||||
if (body.kind() == AgeSkyProfile.Kind.SUN) drawBody(pose, body, time, sky.eclipseFor(body, time));
|
||||
for (AgeSkyProfile.Body body : sky.bodies())
|
||||
if (body.kind() == AgeSkyProfile.Kind.MOON) drawBody(pose, body, time, sky.eclipseFor(body, time));
|
||||
RenderSystem.disableBlend();
|
||||
}
|
||||
setupFog.run();
|
||||
@@ -106,7 +114,7 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void drawBody(PoseStack pose, AgeSkyProfile.Body body, double time) {
|
||||
private static void drawBody(PoseStack pose, AgeSkyProfile.Body body, double time, float eclipse) {
|
||||
pose.pushPose();
|
||||
pose.mulPose(Axis.YP.rotationDegrees(body.angle()));
|
||||
double orbit = body.period() == 0 ? body.phase()
|
||||
@@ -115,24 +123,52 @@ public final class RivencraftSkyEffects extends DimensionSpecialEffects {
|
||||
pose.mulPose(Axis.XP.rotationDegrees((float) (orbit * 360.0D)));
|
||||
float size = (body.kind() == AgeSkyProfile.Kind.SUN ? 30.0F : 20.0F) * body.size();
|
||||
int color = body.color();
|
||||
float red = ((color >> 16) & 255) / 255.0F;
|
||||
float green = ((color >> 8) & 255) / 255.0F;
|
||||
float blue = (color & 255) / 255.0F;
|
||||
if (body.kind() == AgeSkyProfile.Kind.MOON) {
|
||||
red *= 1.0F - eclipse;
|
||||
green *= 1.0F - eclipse;
|
||||
blue *= 1.0F - eclipse;
|
||||
} else {
|
||||
float corona = (float) Math.pow(eclipse, 8.0D) * 0.8F;
|
||||
red += (1.0F - red) * corona;
|
||||
green += (1.0F - green) * corona;
|
||||
blue += (1.0F - blue) * corona;
|
||||
}
|
||||
RenderSystem.setShader(GameRenderer::getPositionTexShader);
|
||||
RenderSystem.setShaderTexture(0, body.kind() == AgeSkyProfile.Kind.SUN ? SUN : MOON);
|
||||
RenderSystem.setShaderColor(((color >> 16) & 255) / 255.0F,
|
||||
((color >> 8) & 255) / 255.0F, (color & 255) / 255.0F, 1.0F);
|
||||
// Vanilla's sun/moon atlases have opaque dark pixels around the bright center.
|
||||
// Additive blending makes those pixels invisible while retaining the halo.
|
||||
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
|
||||
RenderSystem.setShaderColor(red, green, blue,
|
||||
body.kind() == AgeSkyProfile.Kind.SUN ? 0.84F : 0.88F + 0.12F * eclipse);
|
||||
float uMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.25F;
|
||||
float vMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.5F;
|
||||
BufferBuilder vertices = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS,
|
||||
DefaultVertexFormat.POSITION_TEX);
|
||||
Matrix4f matrix = pose.last().pose();
|
||||
vertices.addVertex(matrix, -size, RADIUS - 1.0F, -size).setUv(0.0F, 0.0F);
|
||||
vertices.addVertex(matrix, size, RADIUS - 1.0F, -size).setUv(uMax, 0.0F);
|
||||
vertices.addVertex(matrix, size, RADIUS - 1.0F, size).setUv(uMax, vMax);
|
||||
vertices.addVertex(matrix, -size, RADIUS - 1.0F, size).setUv(0.0F, vMax);
|
||||
BufferUploader.drawWithShader(vertices.buildOrThrow());
|
||||
drawTexturedQuad(pose.last().pose(), -size, size, 0.0F, uMax, 0.0F, vMax);
|
||||
|
||||
// The 8x8 center of each 32x32 sprite is the actual body. Draw it last
|
||||
// with ordinary alpha blending and full opacity so the sky and other
|
||||
// celestial glows cannot show through it, including during an eclipse.
|
||||
RenderSystem.defaultBlendFunc();
|
||||
RenderSystem.setShaderColor(red, green, blue, 1.0F);
|
||||
drawTexturedQuad(pose.last().pose(), -size * 0.25F, size * 0.25F,
|
||||
uMax * 0.375F, uMax * 0.625F, vMax * 0.375F, vMax * 0.625F);
|
||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
pose.popPose();
|
||||
}
|
||||
|
||||
private static void drawTexturedQuad(Matrix4f matrix, float min, float max,
|
||||
float uMin, float uMax, float vMin, float vMax) {
|
||||
BufferBuilder vertices = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS,
|
||||
DefaultVertexFormat.POSITION_TEX);
|
||||
vertices.addVertex(matrix, min, RADIUS - 1.0F, min).setUv(uMin, vMin);
|
||||
vertices.addVertex(matrix, max, RADIUS - 1.0F, min).setUv(uMax, vMin);
|
||||
vertices.addVertex(matrix, max, RADIUS - 1.0F, max).setUv(uMax, vMax);
|
||||
vertices.addVertex(matrix, min, RADIUS - 1.0F, max).setUv(uMin, vMax);
|
||||
BufferUploader.drawWithShader(vertices.buildOrThrow());
|
||||
}
|
||||
|
||||
private static void vertex(BufferBuilder vertices, Matrix4f matrix, int axis, int sign,
|
||||
float x, float y, int color) {
|
||||
float fixed = sign * RADIUS;
|
||||
|
||||
@@ -138,6 +138,17 @@ public final class SymbolPageTextures {
|
||||
y * base.getHeight() / SIZE));
|
||||
}
|
||||
}
|
||||
String numberPath = id.getPath().startsWith("number/")
|
||||
? id.getPath().substring("number/".length()) : "";
|
||||
if (id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& numberPath.matches("(?:[0-9]|1[0-9]|2[0-4])")) {
|
||||
int digit = Integer.parseInt(numberPath);
|
||||
int glyphSize = item ? GLYPH * ITEM_SYMBOL_SIZE / ICON_SIZE : GLYPH;
|
||||
int origin = item ? scaleForItem(38) : 38;
|
||||
for (int component : SymbolNumerals.componentsForDigit(digit))
|
||||
blendComponent(image, component, origin, origin, glyphSize);
|
||||
return image;
|
||||
}
|
||||
String[] words = poem.isBlank() ? generatedWords(id) : poem.split(" ");
|
||||
int[] offsetsX = {38, 77, 38, 0};
|
||||
int[] offsetsY = {0, 38, 77, 38};
|
||||
@@ -146,18 +157,22 @@ public final class SymbolPageTextures {
|
||||
int originX = item ? scaleForItem(offsetsX[word]) : offsetsX[word];
|
||||
int originY = item ? scaleForItem(offsetsY[word]) : offsetsY[word];
|
||||
for (int component : glyphs(words[word])) {
|
||||
for (int y = 0; y < glyphSize; y++) for (int x = 0; x < glyphSize; x++) {
|
||||
int pixel = components.getPixelRGBA((component % 8) * 64 + x * 64 / glyphSize,
|
||||
(component / 8) * 64 + y * 64 / glyphSize);
|
||||
int alpha = pixel >>> 24;
|
||||
if (alpha != 0) image.blendPixel(originX + x, originY + y,
|
||||
(alpha << 24) | 0x001A212B);
|
||||
}
|
||||
blendComponent(image, component, originX, originY, glyphSize);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static void blendComponent(NativeImage image, int component, int originX, int originY, int glyphSize) {
|
||||
for (int y = 0; y < glyphSize; y++) for (int x = 0; x < glyphSize; x++) {
|
||||
int pixel = components.getPixelRGBA((component % 8) * 64 + x * 64 / glyphSize,
|
||||
(component / 8) * 64 + y * 64 / glyphSize);
|
||||
int alpha = pixel >>> 24;
|
||||
if (alpha != 0) image.blendPixel(originX + x, originY + y,
|
||||
(alpha << 24) | 0x001A212B);
|
||||
}
|
||||
}
|
||||
|
||||
private static int scaleForItem(int coordinate) {
|
||||
return SIZE / 2 + Math.round((coordinate - SIZE / 2F) * ITEM_SYMBOL_SIZE / ICON_SIZE);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package net.dnorberg.rivencraft.compat;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.fml.ModList;
|
||||
|
||||
/** Small optional bridge between frozen Age pages and Ruins' dimension gate. */
|
||||
public final class RuinsCompatibility {
|
||||
private static final Set<ResourceLocation> KNOWN_AGES = ConcurrentHashMap.newKeySet();
|
||||
private static final Map<ResourceLocation, Map<ResourceLocation, Integer>> AUTHORED_TEMPLATES =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private RuinsCompatibility() {}
|
||||
|
||||
public static void register() {
|
||||
if (ModList.get().isLoaded("ruins")) RuinsEventAdapter.register();
|
||||
}
|
||||
|
||||
public static void registerAge(AgeDefinition age) {
|
||||
if (!ModList.get().isLoaded("ruins")) return;
|
||||
KNOWN_AGES.add(age.dimensionId());
|
||||
List<AgeFeatureSelection.Choice> choices = age.plan() == null
|
||||
? age.randomFeatures() : AgeFeatureSelection.resolved(age.plan());
|
||||
Map<ResourceLocation, Integer> selected = templateCounts(choices);
|
||||
if (!selected.isEmpty()) {
|
||||
AUTHORED_TEMPLATES.put(age.dimensionId(), selected);
|
||||
} else {
|
||||
AUTHORED_TEMPLATES.remove(age.dimensionId());
|
||||
}
|
||||
}
|
||||
|
||||
/** Called only by the optional Ruins FileHandler mixin. */
|
||||
public static boolean allowsAge(ResourceLocation dimension) {
|
||||
return AUTHORED_TEMPLATES.containsKey(dimension);
|
||||
}
|
||||
|
||||
public static boolean isKnownAge(ResourceLocation dimension) {
|
||||
return KNOWN_AGES.contains(dimension);
|
||||
}
|
||||
|
||||
public static boolean permits(ResourceLocation dimension, ResourceLocation template) {
|
||||
return templateCount(dimension, template) > 0;
|
||||
}
|
||||
|
||||
public static int templateCount(ResourceLocation dimension, ResourceLocation template) {
|
||||
return AUTHORED_TEMPLATES.getOrDefault(dimension, Map.of()).getOrDefault(template, 0);
|
||||
}
|
||||
|
||||
/** Extra pages increase attempts, without making distinct template choices denser. */
|
||||
public static int attemptsPerChunk(ResourceLocation dimension, int configuredAttempts) {
|
||||
return attemptsPerChunk(configuredAttempts, AUTHORED_TEMPLATES.get(dimension));
|
||||
}
|
||||
|
||||
static int attemptsPerChunk(int configuredAttempts, Map<ResourceLocation, Integer> counts) {
|
||||
if (counts == null || counts.isEmpty()) return configuredAttempts;
|
||||
int duplicates = counts.values().stream().mapToInt(count -> count - 1).sum();
|
||||
return (int) Math.min(Math.max(configuredAttempts, 32L), (long) configuredAttempts + duplicates);
|
||||
}
|
||||
|
||||
static Map<ResourceLocation, Integer> templateCounts(List<AgeFeatureSelection.Choice> choices) {
|
||||
Map<ResourceLocation, Integer> counts = new java.util.HashMap<>();
|
||||
for (AgeFeatureSelection.Choice choice : choices) {
|
||||
if (RuinsTemplatePages.contains(choice.id())) counts.merge(choice.id(), 1, Integer::sum);
|
||||
}
|
||||
return Map.copyOf(counts);
|
||||
}
|
||||
|
||||
public static void onServerStopping() {
|
||||
KNOWN_AGES.clear();
|
||||
AUTHORED_TEMPLATES.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package net.dnorberg.rivencraft.compat;
|
||||
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.nio.file.Files;
|
||||
import net.neoforged.fml.loading.FMLPaths;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.neoforged.fml.ModList;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Each discoverable Ruins template must have an authorable page, or none do. */
|
||||
@GameTestHolder("rivencraft")
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class RuinsCompatibilityGameTests {
|
||||
private RuinsCompatibilityGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void ruinsPagesMatchInstalledTemplates(GameTestHelper helper) {
|
||||
var templates = RuinsTemplatePages.templates();
|
||||
if (!ModList.get().isLoaded("ruins")) {
|
||||
helper.assertTrue(templates.isEmpty(), "Ruins pages must be absent without the optional mod");
|
||||
} else {
|
||||
var creative = PageItem.creativeSymbolPages(helper.getLevel().registryAccess(),
|
||||
RuinsTemplatePages.CATEGORY);
|
||||
helper.assertValueEqual(creative.size(), templates.size(),
|
||||
"Creative and JEI must expose exactly one page per Ruins template");
|
||||
for (String name : templates) {
|
||||
var id = RuinsTemplatePages.symbolId(name);
|
||||
helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(id)),
|
||||
"Ruins template " + name + " must be authorable");
|
||||
helper.assertTrue(PageItem.isUsableWriting(PageItem.createCreativeRuinsPage(name)),
|
||||
"Ruins template " + name + " needs a usable page");
|
||||
}
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void duplicatePagesRetainTheirCount(GameTestHelper helper) {
|
||||
if (RuinsTemplatePages.templates().isEmpty()) {
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
var template = RuinsTemplatePages.symbolId(RuinsTemplatePages.templates().getFirst());
|
||||
var counts = RuinsCompatibility.templateCounts(List.of(
|
||||
new AgeFeatureSelection.Choice(template, Optional.empty()),
|
||||
new AgeFeatureSelection.Choice(template, Optional.empty()),
|
||||
new AgeFeatureSelection.Choice(template, Optional.empty())));
|
||||
helper.assertValueEqual(counts.get(template), 3,
|
||||
"Three authored Ruins pages must triple that template's relative selection weight");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void duplicatePagesIncreaseAttemptsWithinBudget(GameTestHelper helper) {
|
||||
var first = RuinsTemplatePages.symbolId("First.tml");
|
||||
var second = RuinsTemplatePages.symbolId("Second.tml");
|
||||
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(6, Map.of(first, 1)), 6,
|
||||
"One Ruins page must retain the configured attempt count");
|
||||
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(6, Map.of(first, 3, second, 1)), 8,
|
||||
"Only duplicate pages must add one attempt each");
|
||||
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(6, Map.of(first, 50)), 32,
|
||||
"Duplicate pages must not exceed the per-chunk attempt budget");
|
||||
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(40, Map.of(first, 2)), 40,
|
||||
"The budget must not lower an intentionally higher Ruins configuration");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredTemplateBypassesOverworldOnlyGate(GameTestHelper helper) throws Exception {
|
||||
if (!ModList.get().isLoaded("ruins")) {
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
String name = "tikihead1.tml";
|
||||
var path = FMLPaths.GAMEDIR.get().resolve("config/ruins_config/generic").resolve(name);
|
||||
if (!Files.exists(path) || !RuinsTemplatePages.contains(RuinsTemplatePages.symbolId(name))) {
|
||||
helper.succeed(); // Custom Ruins packs need not include the bundled Tiki Head.
|
||||
return;
|
||||
}
|
||||
var writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(net.minecraft.resources.ResourceLocation.parse(
|
||||
"rivencraft:terrain/standard"))),
|
||||
WritingEntry.symbol(new SymbolReference(net.minecraft.resources.ResourceLocation.parse(
|
||||
"rivencraft:biome/plains"))),
|
||||
WritingEntry.symbol(new SymbolReference(net.minecraft.resources.ResourceLocation.parse(
|
||||
"rivencraft:biome/single"))),
|
||||
WritingEntry.symbol(new SymbolReference(RuinsTemplatePages.symbolId(name)))));
|
||||
var age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Ruins Tiki Gate"), writing);
|
||||
RuinsCompatibility.registerAge(age);
|
||||
var ruinsClass = Class.forName("atomicstryker.ruins.common.RuinsMod");
|
||||
Object ruins = ruinsClass.getMethod("getInstance").invoke(null);
|
||||
var currentLevel = ruinsClass.getDeclaredField("lastLoadedLevel");
|
||||
currentLevel.setAccessible(true);
|
||||
Object previousLevel = currentLevel.get(ruins);
|
||||
var templateClass = Class.forName("atomicstryker.ruins.common.RuinTemplate");
|
||||
try {
|
||||
currentLevel.set(ruins, helper.getLevel());
|
||||
Object template = templateClass.getConstructor(String.class, String.class)
|
||||
.newInstance(path.toString(), name);
|
||||
boolean accepted = (boolean) templateClass.getMethod("acceptsDimension", String.class)
|
||||
.invoke(template, age.dimensionId().getPath());
|
||||
helper.assertTrue(accepted, "The authored Tiki Head must load despite dimensionsToSpawnIn=overworld");
|
||||
} finally {
|
||||
currentLevel.set(ruins, previousLevel);
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.dnorberg.rivencraft.compat;
|
||||
|
||||
import atomicstryker.ruins.common.EventRuinTemplateSpawn;
|
||||
import atomicstryker.ruins.common.RuinTemplate;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
|
||||
/** Loaded only when Ruins is present. Its pre-spawn event can veto exact templates. */
|
||||
public final class RuinsEventAdapter {
|
||||
private RuinsEventAdapter() {}
|
||||
|
||||
static void register() {
|
||||
NeoForge.EVENT_BUS.addListener(RuinsEventAdapter::onTemplateSpawn);
|
||||
}
|
||||
|
||||
private static void onTemplateSpawn(EventRuinTemplateSpawn event) {
|
||||
if (!event.isPrePhase || event.testingRuin
|
||||
|| !(event.getLevel() instanceof ServerLevel level)
|
||||
|| !RuinsCompatibility.isKnownAge(level.dimension().location())) return;
|
||||
|
||||
var template = RuinsTemplatePages.featureId(event.template.getName());
|
||||
if (!RuinsTemplatePages.contains(template)
|
||||
|| !RuinsCompatibility.permits(level.dimension().location(), template)) event.setCancelled(true);
|
||||
}
|
||||
|
||||
/** Preserve Ruins' weights and multiply each by its authored page count. */
|
||||
public static Object chooseTemplate(net.minecraft.resources.ResourceLocation dimension,
|
||||
Collection<?> pool, RandomSource random) {
|
||||
if (pool == null || pool.isEmpty()) return null;
|
||||
var candidates = pool.stream().filter(RuinTemplate.class::isInstance)
|
||||
.map(RuinTemplate.class::cast)
|
||||
.filter(template -> RuinsCompatibility.permits(dimension,
|
||||
RuinsTemplatePages.featureId(template.getName())))
|
||||
.sorted(Comparator.comparing(RuinTemplate::getName)).toList();
|
||||
double total = candidates.stream().mapToDouble(template -> authoredWeight(dimension, template)).sum();
|
||||
if (total <= 0.0) return null;
|
||||
double roll = random.nextDouble() * total;
|
||||
for (RuinTemplate candidate : candidates) {
|
||||
roll -= authoredWeight(dimension, candidate);
|
||||
if (roll < 0.0) return candidate;
|
||||
}
|
||||
return candidates.getLast();
|
||||
}
|
||||
|
||||
private static double authoredWeight(net.minecraft.resources.ResourceLocation dimension,
|
||||
RuinTemplate template) {
|
||||
return Math.max(0.0, template.getWeight()) * RuinsCompatibility.templateCount(dimension,
|
||||
RuinsTemplatePages.featureId(template.getName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package net.dnorberg.rivencraft.compat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDefinition;
|
||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.fml.ModList;
|
||||
import net.neoforged.fml.loading.FMLPaths;
|
||||
|
||||
/** Discovers Ruins' template filenames without making its jar mandatory. */
|
||||
public final class RuinsTemplatePages {
|
||||
public static final ResourceLocation CATEGORY = ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "ruins");
|
||||
private static final String PREFIX = "ruins/";
|
||||
private static volatile Map<ResourceLocation, String> names = Map.of();
|
||||
private static volatile boolean initialized;
|
||||
|
||||
private RuinsTemplatePages() {}
|
||||
|
||||
public static void refresh() {
|
||||
if (!ModList.get().isLoaded("ruins")) {
|
||||
names = Map.of();
|
||||
initialized = true;
|
||||
return;
|
||||
}
|
||||
Map<ResourceLocation, String> discovered = new HashMap<>();
|
||||
Map<String, String> sources = new HashMap<>();
|
||||
Set<String> duplicateNames = new HashSet<>();
|
||||
// Ruins itself reads <gameDir>/config/ruins_config, not NeoForge's
|
||||
// overrideable CONFIGDIR; mirror its active folder exactly.
|
||||
Path configured = FMLPaths.GAMEDIR.get().resolve("config/ruins_config");
|
||||
if (Files.isDirectory(configured)) {
|
||||
try (var paths = Files.walk(configured)) {
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(path -> configured.relativize(path).getNameCount() == 2)
|
||||
.filter(path -> isTemplate(path.getFileName().toString()))
|
||||
.sorted().forEach(path -> add(discovered, sources, duplicateNames,
|
||||
path.getFileName().toString(), configured.relativize(path).toString()));
|
||||
} catch (IOException exception) {
|
||||
Rivencraft.LOGGER.warn("Could not read Ruins templates from {}", configured, exception);
|
||||
}
|
||||
}
|
||||
// Ruins extracts this archive on first launch. It also supplies the creative
|
||||
// pages before extraction has completed on a fresh installation.
|
||||
if (discovered.isEmpty() && !Files.isDirectory(configured)) {
|
||||
try (InputStream stream = RuinsTemplatePages.class.getClassLoader()
|
||||
.getResourceAsStream("assets/ruins_config.zip")) {
|
||||
if (stream != null) {
|
||||
try (ZipInputStream zip = new ZipInputStream(stream)) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory()) {
|
||||
String name = entry.getName();
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
if (isTemplate(name)) add(discovered, sources, duplicateNames,
|
||||
name, entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
Rivencraft.LOGGER.warn("Could not read bundled Ruins templates", exception);
|
||||
}
|
||||
}
|
||||
// Ruins' spawn event exposes only the basename. If two directories use
|
||||
// that basename, Rivencraft cannot identify which structure was chosen.
|
||||
// Disable the bridge instead of advertising pages with ambiguous effects.
|
||||
if (!duplicateNames.isEmpty()) {
|
||||
Rivencraft.LOGGER.error("Ruins compatibility disabled: duplicate template names {}. "
|
||||
+ "Give every .tml file a unique basename.", duplicateNames);
|
||||
names = Map.of();
|
||||
initialized = true;
|
||||
return;
|
||||
}
|
||||
names = Map.copyOf(discovered);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public static List<String> templates() {
|
||||
if (!initialized) refresh();
|
||||
List<String> templates = new ArrayList<>(names.values());
|
||||
templates.sort(Comparator.comparing(String::toLowerCase));
|
||||
return List.copyOf(templates);
|
||||
}
|
||||
|
||||
public static boolean isSymbol(ResourceLocation id) {
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID) && id.getPath().startsWith(PREFIX);
|
||||
}
|
||||
|
||||
public static boolean contains(ResourceLocation id) {
|
||||
return isSymbol(id) && names.containsKey(id);
|
||||
}
|
||||
|
||||
public static ResourceLocation symbolId(String templateName) {
|
||||
if (templateName.isBlank()) throw new IllegalArgumentException("Ruins template has no usable name");
|
||||
// Hex is reversible and collision-free even for case-sensitive or Unicode
|
||||
// filenames; normalizing to lowercase would merge distinct templates.
|
||||
String key = HexFormat.of().formatHex(templateName.getBytes(StandardCharsets.UTF_8));
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, PREFIX + "t_" + key);
|
||||
}
|
||||
|
||||
public static ResourceLocation featureId(String templateName) {
|
||||
return symbolId(templateName);
|
||||
}
|
||||
|
||||
public static String fallbackName(ResourceLocation symbolId) {
|
||||
String encoded = symbolId.getPath().substring(PREFIX.length());
|
||||
if (!encoded.startsWith("t_")) return encoded + " Ruin";
|
||||
String name;
|
||||
try {
|
||||
name = new String(HexFormat.of().parseHex(encoded.substring(2)), StandardCharsets.UTF_8);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
name = encoded;
|
||||
}
|
||||
return stripExtension(name).replace('_', ' ') + " Ruin";
|
||||
}
|
||||
|
||||
public static SymbolDefinition definition(String templateName) {
|
||||
ResourceLocation id = featureId(templateName);
|
||||
return new SymbolDefinition(SymbolDefinition.CURRENT_SCHEMA_VERSION,
|
||||
"symbol.rivencraft.ruins.template", "symbol.rivencraft.ruins.description",
|
||||
CATEGORY, Optional.of(new SymbolSemantics(SymbolSemantics.Role.FEATURE,
|
||||
new SymbolSemantics.ResourceValue(id))), "");
|
||||
}
|
||||
|
||||
private static boolean isTemplate(String name) {
|
||||
return name.toLowerCase(Locale.ROOT).endsWith(".tml");
|
||||
}
|
||||
|
||||
private static String stripExtension(String name) {
|
||||
return isTemplate(name) ? name.substring(0, name.length() - 4) : name;
|
||||
}
|
||||
|
||||
private static void add(Map<ResourceLocation, String> templates, Map<String, String> sources,
|
||||
Set<String> duplicateNames, String name, String source) {
|
||||
String previousSource = sources.putIfAbsent(name, source);
|
||||
if (previousSource != null && !previousSource.equals(source)) duplicateNames.add(name);
|
||||
ResourceLocation id = symbolId(name);
|
||||
String previous = templates.putIfAbsent(id, name);
|
||||
if (previous != null && !previous.equals(name)) {
|
||||
Rivencraft.LOGGER.warn("Ruins templates {} and {} share page ID {}; using {}",
|
||||
previous, name, id, previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
import net.neoforged.neoforge.network.registration.NetworkRegistry;
|
||||
|
||||
/** Sends saved celestial angles when a client joins or enters an Age. */
|
||||
public final class AgeCelestialAngleSync {
|
||||
@@ -34,6 +35,8 @@ public final class AgeCelestialAngleSync {
|
||||
}
|
||||
|
||||
public static void send(ServerPlayer player, AgeDefinition age) {
|
||||
// GameTest mock players (and clients without this channel) cannot receive this payload.
|
||||
if (!NetworkRegistry.hasChannel(player.connection, AgeCelestialAnglePayload.TYPE.id())) return;
|
||||
PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload(
|
||||
AgeDimensionService.dimensionKey(age), age.traits().celestialAngle(),
|
||||
AgeSkyProfile.forAge(age)));
|
||||
|
||||
@@ -53,6 +53,8 @@ public final class CrystalPortalGameTests {
|
||||
helper.assertTrue(plan.valid(), "A closed crystal frame must form a portal");
|
||||
BookReceptacleBlockEntity holder = (BookReceptacleBlockEntity) level.getBlockEntity(holderPos);
|
||||
holder.setBook(new ItemStack(ModItems.LINKING_BOOK.get()));
|
||||
helper.assertTrue(level.getBlockState(holderPos).getValue(BookReceptacleBlock.HAS_BOOK),
|
||||
"An occupied receptacle must expose its book state to client renderers");
|
||||
LinkTarget destination = new LinkTarget(GlobalPos.of(level.dimension(), centerTarget(helper)), 0, 0);
|
||||
holder.setDestination(destination);
|
||||
BookReceptacleBlockEntity restored = new BookReceptacleBlockEntity(holderPos, holder.getBlockState());
|
||||
@@ -89,6 +91,12 @@ public final class CrystalPortalGameTests {
|
||||
"A north/south opening must only show its two player-facing sides");
|
||||
CrystalPortalNetwork.shutdown(level, holderPos);
|
||||
helper.assertTrue(level.getBlockState(center).isAir(), "Removing the book must release the portal");
|
||||
helper.assertTrue(holder.takeBook().is(ModItems.LINKING_BOOK.get()),
|
||||
"Removing the book must return the stored item");
|
||||
helper.assertTrue(!level.getBlockState(holderPos).getValue(BookReceptacleBlock.HAS_BOOK),
|
||||
"Removing the book must clear the client-visible model state");
|
||||
helper.assertTrue(!holder.getUpdateTag(level.registryAccess()).contains("Book"),
|
||||
"The block-entity update must also clear the old client-side book stack");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||
@@ -71,6 +72,7 @@ public final class PageItem extends Item {
|
||||
categories.add(BlockSymbolPages.CATEGORY);
|
||||
categories.add(BlockSymbolPages.FLUID_CATEGORY);
|
||||
categories.add(CAVE_BIOME_CATEGORY);
|
||||
if (!RuinsTemplatePages.templates().isEmpty()) categories.add(RuinsTemplatePages.CATEGORY);
|
||||
return List.copyOf(categories);
|
||||
}
|
||||
|
||||
@@ -78,6 +80,11 @@ public final class PageItem extends Item {
|
||||
if (category.equals(CAVE_BIOME_CATEGORY)) {
|
||||
return createCreativeBiomePage(ResourceLocation.withDefaultNamespace("lush_caves"));
|
||||
}
|
||||
if (category.equals(RuinsTemplatePages.CATEGORY)) {
|
||||
return RuinsTemplatePages.templates().stream().findFirst()
|
||||
.map(PageItem::createCreativeRuinsPage)
|
||||
.orElseGet(() -> createBundledCreativePage("feature/villages"));
|
||||
}
|
||||
if (category.equals(BlockSymbolPages.CATEGORY) || category.equals(BlockSymbolPages.FLUID_CATEGORY)) {
|
||||
return createCreativeBlockPage(ResourceLocation.withDefaultNamespace(
|
||||
category.equals(BlockSymbolPages.FLUID_CATEGORY) ? "water" : "stone"));
|
||||
@@ -93,6 +100,7 @@ public final class PageItem extends Item {
|
||||
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries) {
|
||||
List<ItemStack> pages = creativeBundledAndBiomePages(registries);
|
||||
BlockSymbolPages.availableBlocks().forEach(id -> pages.add(createCreativeBlockPage(id)));
|
||||
RuinsTemplatePages.templates().forEach(name -> pages.add(createCreativeRuinsPage(name)));
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
@@ -115,6 +123,9 @@ public final class PageItem extends Item {
|
||||
return BlockSymbolPages.availableBlocks(category).stream()
|
||||
.map(PageItem::createCreativeBlockPage).toList();
|
||||
}
|
||||
if (category.equals(RuinsTemplatePages.CATEGORY)) {
|
||||
return RuinsTemplatePages.templates().stream().map(PageItem::createCreativeRuinsPage).toList();
|
||||
}
|
||||
Set<ResourceLocation> caveBiomes = category.equals(BIOME_CATEGORY) || category.equals(CAVE_BIOME_CATEGORY)
|
||||
? caveBiomeIds(registries) : Set.of();
|
||||
return creativeBundledAndBiomePages(registries).stream()
|
||||
@@ -180,6 +191,14 @@ public final class PageItem extends Item {
|
||||
return page;
|
||||
}
|
||||
|
||||
public static ItemStack createCreativeRuinsPage(String templateName) {
|
||||
var symbolId = RuinsTemplatePages.symbolId(templateName);
|
||||
ItemStack page = create(PageContent.symbol(new SymbolReference(symbolId)));
|
||||
page.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(),
|
||||
RuinsTemplatePages.definition(templateName).display(symbolId));
|
||||
return page;
|
||||
}
|
||||
|
||||
private static Map<String, CreativeSymbol> loadCreativeSymbols() {
|
||||
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
||||
try (var stream = PageItem.class.getResourceAsStream(resource)) {
|
||||
@@ -321,6 +340,10 @@ public final class PageItem extends Item {
|
||||
return Component.translatable(metadata.nameTranslationKey()).withStyle(ChatFormatting.OBFUSCATED);
|
||||
}
|
||||
return reference.id().getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& RuinsTemplatePages.isSymbol(reference.id())
|
||||
? Component.translatableWithFallback(metadata.nameTranslationKey(),
|
||||
RuinsTemplatePages.fallbackName(reference.id()))
|
||||
: reference.id().getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& reference.id().getPath().startsWith("biome/")
|
||||
? Component.translatableWithFallback(metadata.nameTranslationKey(),
|
||||
BiomeSymbolPages.fallbackName(reference.id()))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Increase Ruins' per-chunk attempt loop only for Ages with duplicated Ruins pages. */
|
||||
@Pseudo
|
||||
@Mixin(targets = "atomicstryker.ruins.common.RuinGenerator", remap = false)
|
||||
public abstract class RuinsAttemptMixin {
|
||||
@ModifyExpressionValue(method = "generateNormal", at = @At(value = "FIELD",
|
||||
target = "Latomicstryker/ruins/common/FileHandler;triesPerChunkNormal:I"), remap = false)
|
||||
private int rivencraft$normalAttempts(int configured, Level level) {
|
||||
return RuinsCompatibility.attemptsPerChunk(level.dimension().location(), configured);
|
||||
}
|
||||
|
||||
@ModifyExpressionValue(method = "generateNether", at = @At(value = "FIELD",
|
||||
target = "Latomicstryker/ruins/common/FileHandler;triesPerChunkNether:I"), remap = false)
|
||||
private int rivencraft$netherAttempts(int configured, Level level) {
|
||||
return RuinsCompatibility.attemptsPerChunk(level.dimension().location(), configured);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||
import net.dnorberg.rivencraft.compat.RuinsEventAdapter;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Ruins defaults to a fixed vanilla whitelist, so authored Ages need a narrow override. */
|
||||
@Pseudo
|
||||
@Mixin(targets = "atomicstryker.ruins.common.FileHandler", remap = false)
|
||||
public abstract class RuinsDimensionMixin {
|
||||
@Shadow @Final private ResourceLocation dimension;
|
||||
@Shadow @Final private HashMap<String, HashSet<?>> templates;
|
||||
|
||||
@Inject(method = "allowsDimension", at = @At("HEAD"), cancellable = true, remap = false)
|
||||
private void rivencraft$allowAuthoredAge(String path, CallbackInfoReturnable<Boolean> result) {
|
||||
if (RuinsCompatibility.isKnownAge(dimension)) {
|
||||
result.setReturnValue(RuinsCompatibility.allowsAge(dimension));
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "getTemplate", at = @At("HEAD"), cancellable = true, remap = false)
|
||||
private void rivencraft$selectAuthoredTemplate(RandomSource random, String biome,
|
||||
CallbackInfoReturnable<Object> result) {
|
||||
if (RuinsCompatibility.isKnownAge(dimension)) {
|
||||
result.setReturnValue(RuinsEventAdapter.chooseTemplate(dimension, templates.get(biome), random));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** An authored Ruins page overrides a template's vanilla-only dimension list for that Age. */
|
||||
@Pseudo
|
||||
@Mixin(targets = "atomicstryker.ruins.common.RuinTemplate", remap = false)
|
||||
public abstract class RuinsTemplateDimensionMixin {
|
||||
@Shadow @Final private String name;
|
||||
|
||||
@Inject(method = "acceptsDimension", at = @At("HEAD"), cancellable = true, remap = false)
|
||||
private void rivencraft$allowAuthoredAge(String dimensionPath, CallbackInfoReturnable<Boolean> result) {
|
||||
ResourceLocation dimension = ResourceLocation.fromNamespaceAndPath("rivencraft", dimensionPath);
|
||||
if (RuinsCompatibility.isKnownAge(dimension)) {
|
||||
result.setReturnValue(RuinsCompatibility.permits(dimension, RuinsTemplatePages.featureId(name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public final class ModCreativeTabs {
|
||||
.build());
|
||||
|
||||
private static final List<String> CATEGORY_ORDER = List.of(
|
||||
"terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "celestial", "block", "fluid", "feature", "environment", "corrupted");
|
||||
"terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "color", "number", "celestial", "block", "fluid", "feature", "ruins", "environment", "corrupted");
|
||||
|
||||
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
||||
registerSymbolPageTabs();
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -73,6 +74,9 @@ public final class SymbolCatalog {
|
||||
BiomeSymbolPages.definition(id)));
|
||||
BlockSymbolPages.availableBlocks().forEach(id -> loaded.putIfAbsent(
|
||||
BlockSymbolPages.symbolId(id), BlockSymbolPages.definition(id)));
|
||||
RuinsTemplatePages.refresh();
|
||||
RuinsTemplatePages.templates().forEach(name -> loaded.putIfAbsent(
|
||||
RuinsTemplatePages.symbolId(name), RuinsTemplatePages.definition(name)));
|
||||
symbols = Map.copyOf(loaded);
|
||||
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ public record SymbolSemantics(Role role, Value value) {
|
||||
SKY("sky", Kind.CHOICE, "overworld", "nether", "end", "static"),
|
||||
SKY_COLOR("sky_color", Kind.CHOICE,
|
||||
"red", "orange", "yellow", "green", "blue", "purple", "white", "black"),
|
||||
COLOR("color", Kind.CHOICE, "red", "green", "blue", "black", "white"),
|
||||
COLOR_NUMBER("color_number", Kind.NUMBER),
|
||||
WEATHER("weather", Kind.CHOICE,
|
||||
"normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"),
|
||||
SEASON("season", Kind.CHOICE,
|
||||
@@ -116,6 +118,8 @@ public record SymbolSemantics(Role role, Value value) {
|
||||
return switch (this) {
|
||||
case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0;
|
||||
case SIZE_MODIFIER -> number.amount() >= 0.25 && number.amount() <= 3.0;
|
||||
case COLOR_NUMBER -> number.amount() >= 0.0 && number.amount() <= 24.0
|
||||
&& number.amount() == Math.floor(number.amount());
|
||||
case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0;
|
||||
default -> false;
|
||||
};
|
||||
@@ -127,6 +131,7 @@ public record SymbolSemantics(Role role, Value value) {
|
||||
case CHOICE -> "one of " + choices.stream().sorted().toList();
|
||||
case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]"
|
||||
: this == SIZE_MODIFIER ? "a finite number in [0.25, 3]"
|
||||
: this == COLOR_NUMBER ? "an integer D'ni digit in [0, 24]"
|
||||
: "a finite number in [0, 360) degrees";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public final class WritingGrammar {
|
||||
int lastStage = -1;
|
||||
int biomeCount = 0;
|
||||
ParsedSymbol pendingBlock = null;
|
||||
ParsedSymbol pendingColorNumber = null;
|
||||
for (int index = 0; index < writing.entries().size(); index++) {
|
||||
WritingEntry entry = writing.entries().get(index);
|
||||
if (entry.kind() == WritingEntry.Kind.LINK_PANEL) {
|
||||
@@ -73,6 +74,18 @@ public final class WritingGrammar {
|
||||
Role role = semantics.orElseThrow().role();
|
||||
ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow());
|
||||
symbols.add(parsed);
|
||||
if (pendingColorNumber != null && role != Role.COLOR && role != Role.COLOR_NUMBER) {
|
||||
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingColorNumber.index(),
|
||||
"A number page must be immediately followed by a color page"));
|
||||
pendingColorNumber = null;
|
||||
}
|
||||
if (role == Role.COLOR_NUMBER) {
|
||||
if (pendingColorNumber != null) diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER,
|
||||
pendingColorNumber.index(), "A number page must be immediately followed by a color page"));
|
||||
pendingColorNumber = parsed;
|
||||
} else if (role == Role.COLOR) {
|
||||
pendingColorNumber = null;
|
||||
}
|
||||
if (modifier(role)) {
|
||||
// Legacy modifier pages pop and combine an earlier modifier of the same
|
||||
// kind. Only the final pending value can be left dangling.
|
||||
@@ -115,13 +128,15 @@ public final class WritingGrammar {
|
||||
}
|
||||
if (role == Role.BIOME) biomeCount++;
|
||||
int stage = stage(role);
|
||||
if (stage < lastStage && !modifier(role)) {
|
||||
if (stage < lastStage && !modifier(role) && role != Role.COLOR && role != Role.COLOR_NUMBER) {
|
||||
diagnostics.add(new Diagnostic(Code.OUT_OF_ORDER,
|
||||
index, "Move this " + role.id() + " page before later grammar categories"));
|
||||
} else {
|
||||
} else if (role != Role.COLOR && role != Role.COLOR_NUMBER) {
|
||||
lastStage = stage;
|
||||
}
|
||||
}
|
||||
if (pendingColorNumber != null) diagnostics.add(new Diagnostic(Code.DANGLING_MODIFIER,
|
||||
pendingColorNumber.index(), "A number page must be followed by a color page"));
|
||||
if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1,
|
||||
"Add a Link Panel as the first page"));
|
||||
for (ParsedSymbol dangling : pendingModifiers.values()) {
|
||||
@@ -183,6 +198,7 @@ public final class WritingGrammar {
|
||||
case SEASON -> 4;
|
||||
case LIGHTING -> 5;
|
||||
case SKY, SKY_COLOR -> 6;
|
||||
case COLOR, COLOR_NUMBER -> 6;
|
||||
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER,
|
||||
CELESTIAL_COLOR, DIRECTION_MODIFIER -> 7;
|
||||
case CELESTIAL -> 8;
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
{"variants":{"facing=north":{"model":"rivencraft:block/book_receptacle","y":180},"facing=east":{"model":"rivencraft:block/book_receptacle","y":270},"facing=south":{"model":"rivencraft:block/book_receptacle"},"facing=west":{"model":"rivencraft:block/book_receptacle","y":90},"facing=up":{"model":"rivencraft:block/book_receptacle","x":90},"facing=down":{"model":"rivencraft:block/book_receptacle","x":270,"y":180}}}
|
||||
{
|
||||
"multipart": [
|
||||
{"when": {"facing": "north"}, "apply": {"model": "rivencraft:block/book_receptacle", "y": 180}},
|
||||
{"when": {"facing": "east"}, "apply": {"model": "rivencraft:block/book_receptacle", "y": 270}},
|
||||
{"when": {"facing": "south"}, "apply": {"model": "rivencraft:block/book_receptacle"}},
|
||||
{"when": {"facing": "west"}, "apply": {"model": "rivencraft:block/book_receptacle", "y": 90}},
|
||||
{"when": {"facing": "up"}, "apply": {"model": "rivencraft:block/book_receptacle", "x": 90}},
|
||||
{"when": {"facing": "down"}, "apply": {"model": "rivencraft:block/book_receptacle", "x": 270, "y": 180}}
|
||||
]
|
||||
}
|
||||
@@ -225,22 +225,42 @@
|
||||
"symbol.rivencraft.sky.end.description": "Use the End sky and dark preview background.",
|
||||
"symbol.rivencraft.sky.static": "Static Sky",
|
||||
"symbol.rivencraft.sky.static.description": "Fill the sky with shifting television static.",
|
||||
"symbol.rivencraft.sky_color.red": "Red Sky Color",
|
||||
"symbol.rivencraft.sky_color.red.description": "Tint the sky and linked portal red.",
|
||||
"symbol.rivencraft.sky_color.orange": "Orange Sky Color",
|
||||
"symbol.rivencraft.sky_color.orange.description": "Tint the sky and linked portal orange.",
|
||||
"symbol.rivencraft.sky_color.yellow": "Yellow Sky Color",
|
||||
"symbol.rivencraft.sky_color.yellow.description": "Tint the sky and linked portal yellow.",
|
||||
"symbol.rivencraft.sky_color.green": "Green Sky Color",
|
||||
"symbol.rivencraft.sky_color.green.description": "Tint the sky and linked portal green.",
|
||||
"symbol.rivencraft.sky_color.blue": "Blue Sky Color",
|
||||
"symbol.rivencraft.sky_color.blue.description": "Tint the sky and linked portal blue.",
|
||||
"symbol.rivencraft.sky_color.purple": "Purple Sky Color",
|
||||
"symbol.rivencraft.sky_color.purple.description": "Tint the sky and linked portal purple.",
|
||||
"symbol.rivencraft.sky_color.white": "White Sky Color",
|
||||
"symbol.rivencraft.sky_color.white.description": "Tint the sky and linked portal white.",
|
||||
"symbol.rivencraft.sky_color.black": "Black Sky Color",
|
||||
"symbol.rivencraft.sky_color.black.description": "Tint the sky and linked portal near-black.",
|
||||
"symbol.rivencraft.color.red": "Red Color",
|
||||
"symbol.rivencraft.color.red.description": "Add red to the next Sky, Sun, or Moon. A number page directly before this page sets its strength.",
|
||||
"symbol.rivencraft.color.green": "Green Color",
|
||||
"symbol.rivencraft.color.green.description": "Add green to the next Sky, Sun, or Moon. A number page directly before this page sets its strength.",
|
||||
"symbol.rivencraft.color.blue": "Blue Color",
|
||||
"symbol.rivencraft.color.blue.description": "Add blue to the next Sky, Sun, or Moon. A number page directly before this page sets its strength.",
|
||||
"symbol.rivencraft.color.white": "White Color",
|
||||
"symbol.rivencraft.color.white.description": "Add equal brightness to red, green, and blue in the next Sky, Sun, or Moon.",
|
||||
"symbol.rivencraft.color.black": "Black Color",
|
||||
"symbol.rivencraft.color.black.description": "Subtract equal brightness from red, green, and blue in the next Sky, Sun, or Moon.",
|
||||
"symbol.rivencraft.number.0": "D'ni 0",
|
||||
"symbol.rivencraft.number.1": "D'ni 1",
|
||||
"symbol.rivencraft.number.2": "D'ni 2",
|
||||
"symbol.rivencraft.number.3": "D'ni 3",
|
||||
"symbol.rivencraft.number.4": "D'ni 4",
|
||||
"symbol.rivencraft.number.5": "D'ni 5",
|
||||
"symbol.rivencraft.number.6": "D'ni 6",
|
||||
"symbol.rivencraft.number.7": "D'ni 7",
|
||||
"symbol.rivencraft.number.8": "D'ni 8",
|
||||
"symbol.rivencraft.number.9": "D'ni 9",
|
||||
"symbol.rivencraft.number.10": "D'ni 10",
|
||||
"symbol.rivencraft.number.11": "D'ni 11",
|
||||
"symbol.rivencraft.number.12": "D'ni 12",
|
||||
"symbol.rivencraft.number.13": "D'ni 13",
|
||||
"symbol.rivencraft.number.14": "D'ni 14",
|
||||
"symbol.rivencraft.number.15": "D'ni 15",
|
||||
"symbol.rivencraft.number.16": "D'ni 16",
|
||||
"symbol.rivencraft.number.17": "D'ni 17",
|
||||
"symbol.rivencraft.number.18": "D'ni 18",
|
||||
"symbol.rivencraft.number.19": "D'ni 19",
|
||||
"symbol.rivencraft.number.20": "D'ni 20",
|
||||
"symbol.rivencraft.number.21": "D'ni 21",
|
||||
"symbol.rivencraft.number.22": "D'ni 22",
|
||||
"symbol.rivencraft.number.23": "D'ni 23",
|
||||
"symbol.rivencraft.number.24": "D'ni 24",
|
||||
"symbol.rivencraft.number.description": "Set the next color page to this D'ni number times 10. Without a number page, the Age chooses a random number from 0 to 24.",
|
||||
"symbol.rivencraft.weather.normal": "Normal Weather",
|
||||
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
|
||||
"symbol.rivencraft.weather.fast": "Fast Weather",
|
||||
@@ -271,22 +291,6 @@
|
||||
"symbol.rivencraft.celestial.sun.description": "Add one independently moving sun to the sky.",
|
||||
"symbol.rivencraft.celestial.moon": "Moon",
|
||||
"symbol.rivencraft.celestial.moon.description": "Add one independently moving moon to the sky.",
|
||||
"symbol.rivencraft.celestial_color.red": "Red Celestial",
|
||||
"symbol.rivencraft.celestial_color.red.description": "Color the next sun or moon red.",
|
||||
"symbol.rivencraft.celestial_color.orange": "Orange Celestial",
|
||||
"symbol.rivencraft.celestial_color.orange.description": "Color the next sun or moon orange.",
|
||||
"symbol.rivencraft.celestial_color.yellow": "Yellow Celestial",
|
||||
"symbol.rivencraft.celestial_color.yellow.description": "Color the next sun or moon yellow.",
|
||||
"symbol.rivencraft.celestial_color.green": "Green Celestial",
|
||||
"symbol.rivencraft.celestial_color.green.description": "Color the next sun or moon green.",
|
||||
"symbol.rivencraft.celestial_color.blue": "Blue Celestial",
|
||||
"symbol.rivencraft.celestial_color.blue.description": "Color the next sun or moon blue.",
|
||||
"symbol.rivencraft.celestial_color.purple": "Purple Celestial",
|
||||
"symbol.rivencraft.celestial_color.purple.description": "Color the next sun or moon purple.",
|
||||
"symbol.rivencraft.celestial_color.white": "White Celestial",
|
||||
"symbol.rivencraft.celestial_color.white.description": "Color the next sun or moon white.",
|
||||
"symbol.rivencraft.celestial_color.black": "Black Celestial",
|
||||
"symbol.rivencraft.celestial_color.black.description": "Blend black into the next sun or moon's color.",
|
||||
"symbol.rivencraft.size.small": "Small Celestial",
|
||||
"symbol.rivencraft.size.small.description": "Halve the apparent size of the next sun or moon.",
|
||||
"symbol.rivencraft.size.normal": "Normal Celestial Size",
|
||||
@@ -427,6 +431,8 @@
|
||||
"rivencraft.ponder.crystal_portal.text_3": "Insert a bound Linking or Descriptive Book to activate the portal.",
|
||||
"rivencraft.ponder.crystal_portal.text_4": "Step through the portal. Remove the book to close it.",
|
||||
"itemGroup.rivencraft": "Rivencraft",
|
||||
"itemGroup.rivencraft.pages.ruins": "Rivencraft Ruins Pages",
|
||||
"symbol.rivencraft.ruins.description": "Allows this Ruins template to appear in the Age.",
|
||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
|
||||
"itemGroup.rivencraft.pages.biome": "Rivencraft Biome Pages",
|
||||
@@ -436,6 +442,8 @@
|
||||
"itemGroup.rivencraft.pages.season": "Rivencraft Season Pages",
|
||||
"itemGroup.rivencraft.pages.lighting": "Rivencraft Lighting Pages",
|
||||
"itemGroup.rivencraft.pages.sky": "Rivencraft Sky Pages",
|
||||
"itemGroup.rivencraft.pages.color": "Rivencraft Color Pages",
|
||||
"itemGroup.rivencraft.pages.number": "Rivencraft Number Pages",
|
||||
"itemGroup.rivencraft.pages.celestial": "Rivencraft Celestial Pages",
|
||||
"itemGroup.rivencraft.pages.block": "Rivencraft Block Pages",
|
||||
"itemGroup.rivencraft.pages.fluid": "Rivencraft Fluid Pages",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.black","description":"symbol.rivencraft.celestial_color.black.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"black"},"poem":"Transform Black Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.blue","description":"symbol.rivencraft.celestial_color.blue.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"blue"},"poem":"Transform Blue Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.green","description":"symbol.rivencraft.celestial_color.green.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"green"},"poem":"Transform Green Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.orange","description":"symbol.rivencraft.celestial_color.orange.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"orange"},"poem":"Transform Orange Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.purple","description":"symbol.rivencraft.celestial_color.purple.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"purple"},"poem":"Transform Purple Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.red","description":"symbol.rivencraft.celestial_color.red.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"red"},"poem":"Transform Red Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.white","description":"symbol.rivencraft.celestial_color.white.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"white"},"poem":"Transform White Illuminate Motion"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.yellow","description":"symbol.rivencraft.celestial_color.yellow.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"yellow"},"poem":"Transform Yellow Illuminate Motion"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.color.black","description":"symbol.rivencraft.color.black.description","category":"rivencraft:color","semantics":{"role":"color","value":"black"},"poem":"Ethereal Black Transform Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.color.blue","description":"symbol.rivencraft.color.blue.description","category":"rivencraft:color","semantics":{"role":"color","value":"blue"},"poem":"Ethereal Blue Transform Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.color.green","description":"symbol.rivencraft.color.green.description","category":"rivencraft:color","semantics":{"role":"color","value":"green"},"poem":"Ethereal Green Transform Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.color.red","description":"symbol.rivencraft.color.red.description","category":"rivencraft:color","semantics":{"role":"color","value":"red"},"poem":"Ethereal Red Transform Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.color.white","description":"symbol.rivencraft.color.white.description","category":"rivencraft:color","semantics":{"role":"color","value":"white"},"poem":"Ethereal White Transform Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.0","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":0},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.1","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":1},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.10","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":10},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.11","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":11},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.12","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":12},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.13","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":13},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.14","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":14},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.15","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":15},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.16","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":16},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.17","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":17},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.18","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":18},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.19","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":19},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.2","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":2},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.20","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":20},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.21","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":21},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.22","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":22},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.23","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":23},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.24","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":24},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.3","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":3},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.4","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":4},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.5","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":5},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.6","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":6},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.7","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":7},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.8","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":8},"poem":"Transform Time System Balance"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.number.9","description":"symbol.rivencraft.number.description","category":"rivencraft:number","semantics":{"role":"color_number","value":9},"poem":"Transform Time System Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.black","description":"symbol.rivencraft.sky_color.black.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"black"},"poem":"Ethereal Black Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.blue","description":"symbol.rivencraft.sky_color.blue.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"blue"},"poem":"Ethereal Blue Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.green","description":"symbol.rivencraft.sky_color.green.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"green"},"poem":"Ethereal Green Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.orange","description":"symbol.rivencraft.sky_color.orange.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"orange"},"poem":"Ethereal Orange Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.purple","description":"symbol.rivencraft.sky_color.purple.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"purple"},"poem":"Ethereal Purple Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.red","description":"symbol.rivencraft.sky_color.red.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"red"},"poem":"Ethereal Red Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.white","description":"symbol.rivencraft.sky_color.white.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"white"},"poem":"Ethereal White Sky Balance"}
|
||||
@@ -1 +0,0 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.sky_color.yellow","description":"symbol.rivencraft.sky_color.yellow.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"yellow"},"poem":"Ethereal Yellow Sky Balance"}
|
||||
@@ -9,6 +9,9 @@
|
||||
"AgeDungeonPlacementMixin",
|
||||
"AgeStructureBiomeMixin",
|
||||
"MobDropChanceAccessor",
|
||||
"RuinsDimensionMixin",
|
||||
"RuinsAttemptMixin",
|
||||
"RuinsTemplateDimensionMixin",
|
||||
"StructurePlacementAccessor"
|
||||
],
|
||||
"client": [
|
||||
|
||||
Reference in new issue
Block a user