Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac4c499ab | ||
|
|
9044835ec5 | ||
|
|
7ae41da23e | ||
|
|
9cb63e6994 | ||
|
|
6337b5aa10 |
No files matched your search
@@ -118,6 +118,22 @@ book or page is placed on it; this in-world interaction has no JEI guide.
|
|||||||
With Create installed, Crystal Blocks and Book Receptacles have an optional Ponder scene
|
With Create installed, Crystal Blocks and Book Receptacles have an optional Ponder scene
|
||||||
showing crystal-frame construction, book activation, and a parrot crossing the portal. Use the Ponder shortcut
|
showing crystal-frame construction, book activation, and a parrot crossing the portal. Use the Ponder shortcut
|
||||||
while hovering either item; Rivencraft does not require Create to run.
|
while hovering either item; Rivencraft does not require Create to run.
|
||||||
|
Crystal frames may stand vertically or lie flat: a 3×3 Crystal Block ring with
|
||||||
|
an empty center forms a horizontal portal when a Book Receptacle is attached to
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
color, and celestial arrangement from their Age seed; older saved Ages retain
|
||||||
|
their previous sky.
|
||||||
|
|
||||||
Craft a Book Binder from three iron ingots and five planks. Put leather or an
|
Craft a Book Binder from three iron ingots and five planks. Put leather or an
|
||||||
empty Notebook in the left cover slot. Enter a title, then place a Link Panel
|
empty Notebook in the left cover slot. Enter a title, then place a Link Panel
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 47 KiB |
@@ -85,8 +85,9 @@ With optional JEI installed, search for the Ink Mixer category. It should show
|
|||||||
the base paper/ink/vial operation and separate entries for Gunpowder, Mushroom
|
the base paper/ink/vial operation and separate entries for Gunpowder, Mushroom
|
||||||
Stew, Experience Bottle, Ender Pearl, Feather, and Fire Charge,
|
Stew, Experience Bottle, Ender Pearl, Feather, and Fire Charge,
|
||||||
with their property chances. Black Dye and Clay Ball should not appear: the
|
with their property chances. Black Dye and Clay Ball should not appear: the
|
||||||
former had no working Legacy effect, while Generate Platform is redundant
|
former had no working Legacy effect, while Clay Ball remains unsupported by
|
||||||
with default Age arrival. Without JEI, Rivencraft should still
|
the mixer. The creative Link Modifier can enable a single arrival platform for
|
||||||
|
Linking Books; they do not create one by default. Without JEI, Rivencraft should still
|
||||||
start and the mixer should work normally.
|
start and the mixer should work normally.
|
||||||
|
|
||||||
Put paper and an Ink Vial in the mixer. Hold Gunpowder on the cursor and
|
Put paper and an Ink Vial in the mixer. Hold Gunpowder on the cursor and
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ Implementation requirements: keep the mixer server-authoritative; persist mixtur
|
|||||||
|
|
||||||
The built-in vanilla-item rows above are supported except black dye and clay balls. Legacy ignores black dye's empty effect key, so Rivencraft does not consume or advertise it in JEI. Clay Balls are omitted because Age arrival already builds a safe platform by default and a separate Generate Platform link modifier would be redundant. Right-clicking the Ink Mixer basin with a carried item adds one; left-clicking adds the carried stack. The mixture is persistent and its next panel is deterministic. The visual basin tooltip shows current probabilities. Only taking the virtual Link Panel consumes paper/ink and clears the mixture. The Link Panel's property component is copied to a Linking Book by the leather recipe or a Descriptive Book by the Binder. JEI's optional Ink Mixer category lists the base panel and each supported additive interaction with its per-property chance; Rivencraft does not require JEI to run.
|
The built-in vanilla-item rows above are supported except black dye and clay balls. Legacy ignores black dye's empty effect key, so Rivencraft does not consume or advertise it in JEI. Clay Balls are omitted because Age arrival already builds a safe platform by default and a separate Generate Platform link modifier would be redundant. Right-clicking the Ink Mixer basin with a carried item adds one; left-clicking adds the carried stack. The mixture is persistent and its next panel is deterministic. The visual basin tooltip shows current probabilities. Only taking the virtual Link Panel consumes paper/ink and clears the mixture. The Link Panel's property component is copied to a Linking Book by the leather recipe or a Descriptive Book by the Binder. JEI's optional Ink Mixer category lists the base panel and each supported additive interaction with its per-property chance; Rivencraft does not require JEI to run.
|
||||||
|
|
||||||
Disarm and Maintain Momentum are applied by the server after confirming a safe destination. Disarm spawns each inventory stack as an item entity at the departure point, including for Descriptive Books. Intra Linking is required only for a Linking Book whose destination is in the player's current dimension; ordinary cross-dimensional Linking Books still work. The Binder treats an Intra Linking panel as an ordinary Link Panel and omits that flag from the resulting Descriptive Book, while retaining Disarm and Maintain Momentum. Automatic Return Links remain enabled under the existing configuration, as requested. The old Generate Platform bit remains readable on pre-0.4.7.1 test items but is neither rolled nor applied. Optional dust-tag mappings remain outside this built-in subset until their cross-mod tags and gameplay meaning are agreed.
|
Disarm and Maintain Momentum are applied by the server after confirming a safe destination. Disarm spawns each inventory stack as an item entity at the departure point, including for Descriptive Books. Intra Linking is required only for a Linking Book whose destination is in the player's current dimension; ordinary cross-dimensional Linking Books still work. The Binder treats an Intra Linking panel as an ordinary Link Panel and omits that flag from the resulting Descriptive Book, while retaining Disarm and Maintain Momentum. Automatic Return Links remain enabled under the existing configuration, as requested. The Ink Mixer still does not roll Generate Platform, but since 0.6.7.1 the creative Link Modifier can explicitly enable it for Linking Books; their default remains off. Optional dust-tag mappings remain outside this built-in subset until their cross-mod tags and gameplay meaning are agreed.
|
||||||
@@ -20,7 +20,7 @@ Milestone 5 issue #29 adds typed meaning to the existing data-pack symbol catalo
|
|||||||
|
|
||||||
`poem` is optional, client-safe artwork metadata: exactly four alphabetic words, ordered top, right, bottom, left. The client layers their glyphs from the shared Legacy component sheet at runtime. GUI thumbnails use the full transparent artwork over their page backgrounds; item icons use the same artwork reduced from 16 to 13 pixels across, over the page texture. Unregistered words use a stable generated component combination; when a symbol has no poem, the six original built-in IDs retain their Legacy poems and other IDs produce four stable fallback words. This affects appearance only, never Age generation. Generated images are cached up to 256 entries and discarded on resource reload.
|
`poem` is optional, client-safe artwork metadata: exactly four alphabetic words, ordered top, right, bottom, left. The client layers their glyphs from the shared Legacy component sheet at runtime. GUI thumbnails use the full transparent artwork over their page backgrounds; item icons use the same artwork reduced from 16 to 13 pixels across, over the page texture. Unregistered words use a stable generated component combination; when a symbol has no poem, the six original built-in IDs retain their Legacy poems and other IDs produce four stable fallback words. This affects appearance only, never Age generation. Generated images are cached up to 256 entries and discarded on resource reload.
|
||||||
|
|
||||||
The Writing Desk's 88 collection tabs use Legacy's number components, chiefly the bottom row of `symbolcomponents.png`, rather than word sigils. Legacy's `ModWords.constructNumber` combines four unit marks (1–4) with four five-step marks (5–20) to form 1–24 and uses a separate zero mark. Rivencraft extends these marks positionally in base 25 for tabs 25–88 (for example, 25 is 1 followed by 0). Hovering still reveals the decimal number for accessibility. The former arbitrary-word glyph generator remains useful for rendering text: on page-free Descriptive Books, the generated Age description now appears as a sequence of layered word glyphs instead of Minecraft's enchantment font. The Age name stays readable.
|
The Writing Desk retains 88 collection tabs, numbered from zero as in Legacy. Legacy's `ModWords.constructNumber` defines 0 as component 1 and 1–24 by overlaying a unit mark (1–4) and a five-step mark (5–20) from the bottom row of `symbolcomponents.png` in the *same square*. Legacy's Writing Desk has 25 tabs (0–24) and scrolls its four-tab window **one tab at a time**. Thus the supplied Legacy screenshots labeled 1–4, 5–8, and so on show overlapping windows, not 88 distinct numerals. Rivencraft retains its previously requested four-at-a-time navigation and 88 tabs. Its additional tab numbers use positional base 25: each place is an overlaid Legacy digit, with the places side by side (25 = 1,0). The two glyphs are sized and positioned to fit the tab. Hovering reveals the decimal tab index for accessibility. The generated-word mechanism renders page-free Descriptive Book text as layered glyphs instead of Minecraft's enchantment font. The Age name stays readable.
|
||||||
|
|
||||||
| Role | Value type and allowed values |
|
| Role | Value type and allowed values |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
@@ -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.
|
# 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
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.6.7.0
|
mod_version=0.6.8.7
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
terrablender_version=4.1.0.8
|
terrablender_version=4.1.0.8
|
||||||
serene_seasons_version_id=pHEgQQUE
|
serene_seasons_version_id=pHEgQQUE
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
|
|||||||
}
|
}
|
||||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
|
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||||
Direction face = context.getClickedFace();
|
Direction face = context.getClickedFace();
|
||||||
if (face == Direction.DOWN || !context.getLevel().getBlockState(
|
if (!context.getLevel().getBlockState(
|
||||||
context.getClickedPos().relative(face.getOpposite())).is(ModBlocks.CRYSTAL_BLOCK.get())) return null;
|
context.getClickedPos().relative(face.getOpposite())).is(ModBlocks.CRYSTAL_BLOCK.get())) return null;
|
||||||
return defaultBlockState().setValue(FACING, face);
|
return defaultBlockState().setValue(FACING, face);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import net.dnorberg.rivencraft.registry.ModItems;
|
|||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.Direction;
|
import net.minecraft.core.Direction;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraft.util.RandomSource;
|
import net.minecraft.util.RandomSource;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
@@ -54,16 +53,16 @@ public final class CrystalPortalBlock extends BaseEntityBlock {
|
|||||||
@Override protected boolean canBeReplaced(BlockState state, Fluid fluid) { return false; }
|
@Override protected boolean canBeReplaced(BlockState state, Fluid fluid) { return false; }
|
||||||
|
|
||||||
@Override protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
|
@Override protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
|
||||||
if (!(level instanceof ServerLevel server) || !(entity instanceof ServerPlayer player)) return;
|
if (!(level instanceof ServerLevel server)) return;
|
||||||
long now = server.getServer().getTickCount();
|
long now = server.getServer().getTickCount();
|
||||||
if (PortalTravelCooldown.blocked(player, now)) return;
|
if (PortalTravelCooldown.blocked(entity, now)) return;
|
||||||
if (!(level.getBlockEntity(pos) instanceof CrystalPortalBlockEntity portal)
|
if (!(level.getBlockEntity(pos) instanceof CrystalPortalBlockEntity portal)
|
||||||
|| portal.receptacle() == null || !level.hasChunkAt(portal.receptacle())
|
|| portal.receptacle() == null || !level.hasChunkAt(portal.receptacle())
|
||||||
|| !(level.getBlockEntity(portal.receptacle()) instanceof BookReceptacleBlockEntity holder)) return;
|
|| !(level.getBlockEntity(portal.receptacle()) instanceof BookReceptacleBlockEntity holder)) return;
|
||||||
ItemStack book = holder.getBook();
|
ItemStack book = holder.getBook();
|
||||||
if (book.isEmpty()) return;
|
if (book.isEmpty()) return;
|
||||||
PortalTravelCooldown.mark(player, now);
|
PortalTravelCooldown.mark(entity, now);
|
||||||
PortalTravelQueue.enqueue(player, server, pos, portal.receptacle(), book, now);
|
PortalTravelQueue.enqueue(entity, server, pos, portal.receptacle(), book, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbor,
|
@Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbor,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.block.entity;
|
package net.dnorberg.rivencraft.block.entity;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeEnvironmentCompiler;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||||
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
|
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
|
||||||
import net.minecraft.nbt.NbtOps;
|
import net.minecraft.nbt.NbtOps;
|
||||||
@@ -20,8 +21,10 @@ import net.neoforged.fml.ModList;
|
|||||||
public final class BookReceptacleBlockEntity extends BlockEntity {
|
public final class BookReceptacleBlockEntity extends BlockEntity {
|
||||||
private static final String BOOK_TAG = "Book";
|
private static final String BOOK_TAG = "Book";
|
||||||
private static final String DESTINATION_TAG = "Destination";
|
private static final String DESTINATION_TAG = "Destination";
|
||||||
|
private static final String SKY_COLOR_TAG = "SkyColor";
|
||||||
private ItemStack book = ItemStack.EMPTY;
|
private ItemStack book = ItemStack.EMPTY;
|
||||||
private LinkTarget destination;
|
private LinkTarget destination;
|
||||||
|
private int skyColor = 0xFFFFFF;
|
||||||
private boolean restoreView = true;
|
private boolean restoreView = true;
|
||||||
|
|
||||||
public BookReceptacleBlockEntity(BlockPos pos, BlockState state) {
|
public BookReceptacleBlockEntity(BlockPos pos, BlockState state) {
|
||||||
@@ -30,6 +33,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
|||||||
|
|
||||||
public ItemStack getBook() { return book; }
|
public ItemStack getBook() { return book; }
|
||||||
public LinkTarget destination() { return destination; }
|
public LinkTarget destination() { return destination; }
|
||||||
|
public int skyColor() { return skyColor; }
|
||||||
|
|
||||||
public void setBook(ItemStack stack) {
|
public void setBook(ItemStack stack) {
|
||||||
book = stack.copyWithCount(1);
|
book = stack.copyWithCount(1);
|
||||||
@@ -38,6 +42,12 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
|||||||
|
|
||||||
public void setDestination(LinkTarget target) {
|
public void setDestination(LinkTarget target) {
|
||||||
destination = target;
|
destination = target;
|
||||||
|
skyColor = 0xFFFFFF;
|
||||||
|
if (target != null && level instanceof ServerLevel server) {
|
||||||
|
ServerLevel targetLevel = server.getServer().getLevel(target.location().dimension());
|
||||||
|
if (targetLevel != null) skyColor = AgeEnvironmentCompiler.previewSkyColor(
|
||||||
|
targetLevel, target.location().pos()) & 0xFFFFFF;
|
||||||
|
}
|
||||||
sync();
|
sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +55,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
|||||||
ItemStack result = book;
|
ItemStack result = book;
|
||||||
book = ItemStack.EMPTY;
|
book = ItemStack.EMPTY;
|
||||||
destination = null;
|
destination = null;
|
||||||
|
skyColor = 0xFFFFFF;
|
||||||
sync();
|
sync();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -63,6 +74,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
|||||||
: ItemStack.EMPTY;
|
: ItemStack.EMPTY;
|
||||||
destination = tag.contains(DESTINATION_TAG)
|
destination = tag.contains(DESTINATION_TAG)
|
||||||
? LinkTarget.CODEC.parse(NbtOps.INSTANCE, tag.get(DESTINATION_TAG)).result().orElse(null) : null;
|
? LinkTarget.CODEC.parse(NbtOps.INSTANCE, tag.get(DESTINATION_TAG)).result().orElse(null) : null;
|
||||||
|
skyColor = tag.contains(SKY_COLOR_TAG) ? tag.getInt(SKY_COLOR_TAG) & 0xFFFFFF : 0xFFFFFF;
|
||||||
restoreView = true;
|
restoreView = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
|
|||||||
if (!book.isEmpty()) tag.put(BOOK_TAG, book.save(registries));
|
if (!book.isEmpty()) tag.put(BOOK_TAG, book.save(registries));
|
||||||
if (destination != null) LinkTarget.CODEC.encodeStart(NbtOps.INSTANCE, destination)
|
if (destination != null) LinkTarget.CODEC.encodeStart(NbtOps.INSTANCE, destination)
|
||||||
.result().ifPresent(encoded -> tag.put(DESTINATION_TAG, encoded));
|
.result().ifPresent(encoded -> tag.put(DESTINATION_TAG, encoded));
|
||||||
|
if (destination != null) tag.putInt(SKY_COLOR_TAG, skyColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public Packet<ClientGamePacketListener> getUpdatePacket() {
|
@Override public Packet<ClientGamePacketListener> getUpdatePacket() {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ public record AgeDefinition(
|
|||||||
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
|
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "random_vanilla"
|
Rivencraft.MOD_ID, "random_vanilla"
|
||||||
);
|
);
|
||||||
public static final int CURRENT_GENERATION_VERSION = 16;
|
public static final int LEGACY_GENERATOR_CUTOFF = 16;
|
||||||
|
public static final int CURRENT_GENERATION_VERSION = AgeSkyProfile.RANDOM_SKY_GENERATION_VERSION;
|
||||||
|
|
||||||
public AgeDefinition {
|
public AgeDefinition {
|
||||||
Objects.requireNonNull(id, "id");
|
Objects.requireNonNull(id, "id");
|
||||||
|
|||||||
@@ -101,9 +101,14 @@ public final class AgeDimensionService {
|
|||||||
|
|
||||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
|
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
|
||||||
OrderedWriting writing, boolean generatePlatform) {
|
OrderedWriting writing, boolean generatePlatform) {
|
||||||
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference, writing);
|
return resolve(player.serverLevel(), reference, writing, generatePlatform);
|
||||||
if (AgeRegistrySavedData.get(player.serverLevel()).isDead(definition.id())) return Optional.empty();
|
}
|
||||||
ServerLevel level = ensureLoaded(player.getServer(), definition);
|
|
||||||
|
public static Optional<LinkTarget> resolve(ServerLevel source, AgeReference reference,
|
||||||
|
OrderedWriting writing, boolean generatePlatform) {
|
||||||
|
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(source, reference, writing);
|
||||||
|
if (AgeRegistrySavedData.get(source).isDead(definition.id())) return Optional.empty();
|
||||||
|
ServerLevel level = ensureLoaded(source.getServer(), definition);
|
||||||
if (level == null) {
|
if (level == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@@ -347,7 +352,7 @@ public final class AgeDimensionService {
|
|||||||
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
|
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
|
||||||
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||||
);
|
);
|
||||||
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION
|
return age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF
|
||||||
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols);
|
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols);
|
||||||
}
|
}
|
||||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
||||||
@@ -357,7 +362,7 @@ public final class AgeDimensionService {
|
|||||||
List.of()
|
List.of()
|
||||||
);
|
);
|
||||||
settings.updateLayers();
|
settings.updateLayers();
|
||||||
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION
|
return age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF
|
||||||
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols);
|
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +372,7 @@ public final class AgeDimensionService {
|
|||||||
NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value();
|
NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value();
|
||||||
Holder<NoiseGeneratorSettings> settings = Holder.direct(withBiomeMaterialRules(baseSettings));
|
Holder<NoiseGeneratorSettings> settings = Holder.direct(withBiomeMaterialRules(baseSettings));
|
||||||
boolean caveTerrain = AgeBiomeSelection.isCave(profile);
|
boolean caveTerrain = AgeBiomeSelection.isCave(profile);
|
||||||
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION
|
return age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF
|
||||||
? new NoiseBasedChunkGenerator(biomeSource, settings)
|
? new NoiseBasedChunkGenerator(biomeSource, settings)
|
||||||
: new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed(),
|
: new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed(),
|
||||||
caveTerrain, caveTerrain ? TerraBlenderBiomes.nether(server.registryAccess()) : List.of(),
|
caveTerrain, caveTerrain ? TerraBlenderBiomes.nether(server.registryAccess()) : List.of(),
|
||||||
@@ -450,7 +455,7 @@ public final class AgeDimensionService {
|
|||||||
AgeDefinition age,
|
AgeDefinition age,
|
||||||
AgeRuntimeProfile profile
|
AgeRuntimeProfile profile
|
||||||
) {
|
) {
|
||||||
if (age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION) {
|
if (age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF) {
|
||||||
return legacyBiomeSource(server, age, profile);
|
return legacyBiomeSource(server, age, profile);
|
||||||
}
|
}
|
||||||
AgeBiomeSelection selection = age.plan() != null && age.plan().biomeSelection() != null
|
AgeBiomeSelection selection = age.plan() != null && age.plan().biomeSelection() != null
|
||||||
@@ -553,7 +558,9 @@ public final class AgeDimensionService {
|
|||||||
DimensionType base = server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE)
|
DimensionType base = server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE)
|
||||||
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType())).value();
|
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType())).value();
|
||||||
AgeTraits.Lighting lighting = age.traits().lighting();
|
AgeTraits.Lighting lighting = age.traits().lighting();
|
||||||
boolean hasSkyLight = base.hasSkyLight() && lighting != AgeTraits.Lighting.DARK;
|
AgeSkyProfile sky = AgeSkyProfile.forAge(age);
|
||||||
|
boolean hasSkyLight = (base.hasSkyLight() || !sky.bodies().isEmpty())
|
||||||
|
&& lighting != AgeTraits.Lighting.DARK && !age.caveTerrain();
|
||||||
float ambientLight = lighting == AgeTraits.Lighting.BRIGHT ? 0.25F : 0.0F;
|
float ambientLight = lighting == AgeTraits.Lighting.BRIGHT ? 0.25F : 0.0F;
|
||||||
return new DimensionType(
|
return new DimensionType(
|
||||||
base.fixedTime(),
|
base.fixedTime(),
|
||||||
@@ -568,7 +575,9 @@ public final class AgeDimensionService {
|
|||||||
base.height(),
|
base.height(),
|
||||||
base.logicalHeight(),
|
base.logicalHeight(),
|
||||||
base.infiniburn(),
|
base.infiniburn(),
|
||||||
AgeEnvironmentCompiler.skyEffects(age.plan(), base.effectsLocation()),
|
age.plan() == null && sky.custom()
|
||||||
|
? AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS
|
||||||
|
: AgeEnvironmentCompiler.skyEffects(age.plan(), base.effectsLocation()),
|
||||||
ambientLight,
|
ambientLight,
|
||||||
base.monsterSettings()
|
base.monsterSettings()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package net.dnorberg.rivencraft.book;
|
|||||||
|
|
||||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||||
|
|
||||||
/** Resolves written environment pages without changing the seeded page-free Age path. */
|
/** Resolves written environment pages without changing the seeded page-free Age path. */
|
||||||
public final class AgeEnvironmentCompiler {
|
public final class AgeEnvironmentCompiler {
|
||||||
|
public static final ResourceLocation CUSTOM_SKY_EFFECTS = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
"rivencraft", "custom_sky");
|
||||||
private AgeEnvironmentCompiler() {}
|
private AgeEnvironmentCompiler() {}
|
||||||
|
|
||||||
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
|
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
|
||||||
@@ -19,6 +25,7 @@ public final class AgeEnvironmentCompiler {
|
|||||||
} else {
|
} else {
|
||||||
applied = applyLegacy(plan, seeded);
|
applied = applyLegacy(plan, seeded);
|
||||||
}
|
}
|
||||||
|
AgeSkyProfile.compile(plan, applied);
|
||||||
return forTerrain(applied, plan.runtimeProfile());
|
return forTerrain(applied, plan.runtimeProfile());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +74,7 @@ public final class AgeEnvironmentCompiler {
|
|||||||
phaseDegrees = writtenPhase ? circularMidpoint(phaseDegrees, next) : next;
|
phaseDegrees = writtenPhase ? circularMidpoint(phaseDegrees, next) : next;
|
||||||
writtenPhase = true;
|
writtenPhase = true;
|
||||||
}
|
}
|
||||||
case ANGLE_MODIFIER -> throw new IllegalArgumentException(
|
case ANGLE_MODIFIER -> { } // Consumed by the next independently rendered celestial.
|
||||||
"Sun angle pages require a custom sky renderer and are not supported yet");
|
|
||||||
default -> { }
|
default -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +87,8 @@ public final class AgeEnvironmentCompiler {
|
|||||||
/** Vanilla effects are the sky styles the current renderer can honor exactly. */
|
/** Vanilla effects are the sky styles the current renderer can honor exactly. */
|
||||||
public static ResourceLocation skyEffects(AgeGenerationPlan plan, ResourceLocation fallback) {
|
public static ResourceLocation skyEffects(AgeGenerationPlan plan, ResourceLocation fallback) {
|
||||||
if (plan == null) return fallback;
|
if (plan == null) return fallback;
|
||||||
|
if (skyColor(plan) != -1 || staticSky(plan) || AgeSkyProfile.hasCustomCelestials(plan))
|
||||||
|
return CUSTOM_SKY_EFFECTS;
|
||||||
return plan.activeSymbols().stream()
|
return plan.activeSymbols().stream()
|
||||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY)
|
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY)
|
||||||
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
|
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
|
||||||
@@ -92,6 +100,52 @@ public final class AgeEnvironmentCompiler {
|
|||||||
}).findFirst().orElse(fallback);
|
}).findFirst().orElse(fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean staticSky(AgeGenerationPlan plan) {
|
||||||
|
return plan != null && plan.activeSymbols().stream().anyMatch(symbol ->
|
||||||
|
symbol.semantics().role() == SymbolSemantics.Role.SKY
|
||||||
|
&& ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("static"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int selectedSkyColor(AgeGenerationPlan plan, ResourceLocation fallback, int biomeSkyColor) {
|
||||||
|
int authored = skyColor(plan);
|
||||||
|
if (authored != -1) return authored;
|
||||||
|
if (staticSky(plan)) return 0xFF929292;
|
||||||
|
ResourceLocation effects = skyEffects(plan, fallback);
|
||||||
|
return previewSkyColor(effects, biomeSkyColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared server answer for book previews and the fallback crystal portal surface. */
|
||||||
|
public static int previewSkyColor(ServerLevel level, BlockPos origin) {
|
||||||
|
int biomeColor = level.getBiome(origin).value().getSkyColor();
|
||||||
|
var age = AgeRegistrySavedData.get(level).definitions().stream()
|
||||||
|
.filter(candidate -> AgeDimensionService.dimensionKey(candidate).equals(level.dimension()))
|
||||||
|
.findFirst();
|
||||||
|
if (age.isEmpty()) return previewSkyColor(level.dimensionType().effectsLocation(), biomeColor);
|
||||||
|
var definition = age.orElseThrow();
|
||||||
|
AgeSkyProfile sky = AgeSkyProfile.forAge(definition);
|
||||||
|
if (definition.plan() == null && definition.generationVersion() >= AgeSkyProfile.RANDOM_SKY_GENERATION_VERSION) {
|
||||||
|
if (sky.color() != -1) return sky.color();
|
||||||
|
if (sky.style() == AgeSkyProfile.Style.STATIC) return 0xFF929292;
|
||||||
|
}
|
||||||
|
var profile = definition.plan() == null
|
||||||
|
? AgeRuntimeProfiles.resolve(definition.generationProfile())
|
||||||
|
: definition.plan().runtimeProfile();
|
||||||
|
ResourceLocation baseEffects = level.getServer().registryAccess()
|
||||||
|
.lookupOrThrow(Registries.DIMENSION_TYPE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType()))
|
||||||
|
.value().effectsLocation();
|
||||||
|
return selectedSkyColor(definition.plan(), baseEffects, biomeColor);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean hasWrittenPhase(AgeGenerationPlan plan) {
|
public static boolean hasWrittenPhase(AgeGenerationPlan plan) {
|
||||||
if (plan == null) return false;
|
if (plan == null) return false;
|
||||||
if (plan.version() >= 4) return orderedCelestial(plan, null).writtenPhase();
|
if (plan.version() >= 4) return orderedCelestial(plan, null).writtenPhase();
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
|||||||
String raw = item.getString("Value");
|
String raw = item.getString("Value");
|
||||||
SymbolSemantics.Value value = switch (role) {
|
SymbolSemantics.Value value = switch (role) {
|
||||||
case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw));
|
case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw));
|
||||||
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER ->
|
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER ->
|
||||||
new SymbolSemantics.NumberValue(Double.parseDouble(raw));
|
new SymbolSemantics.NumberValue(Double.parseDouble(raw));
|
||||||
default -> new SymbolSemantics.ChoiceValue(raw);
|
default -> new SymbolSemantics.ChoiceValue(raw);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
|||||||
}
|
}
|
||||||
|
|
||||||
static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits, RegistryAccess registries) {
|
static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits, RegistryAccess registries) {
|
||||||
|
return compute(plan, traits, registries, Long.MIN_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits, RegistryAccess registries, long ageSeed) {
|
||||||
Objects.requireNonNull(traits, "traits");
|
Objects.requireNonNull(traits, "traits");
|
||||||
List<Contribution> contributions = new ArrayList<>();
|
List<Contribution> contributions = new ArrayList<>();
|
||||||
if (plan == null) {
|
if (plan == null) {
|
||||||
@@ -82,6 +86,17 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
|||||||
contributions.add(new Contribution("bright_lighting", -1, BRIGHT_LIGHTING_COST,
|
contributions.add(new Contribution("bright_lighting", -1, BRIGHT_LIGHTING_COST,
|
||||||
"Seeded bright lighting"));
|
"Seeded bright lighting"));
|
||||||
}
|
}
|
||||||
|
AgeSkyProfile sky = plan == null && ageSeed != Long.MIN_VALUE ? AgeSkyProfile.random(ageSeed, traits)
|
||||||
|
: plan == null ? new AgeSkyProfile(AgeSkyProfile.Style.INHERITED, -1, List.of())
|
||||||
|
: AgeSkyProfile.compile(plan, traits);
|
||||||
|
int seenSuns = 0;
|
||||||
|
for (AgeSkyProfile.Body body : sky.bodies()) {
|
||||||
|
if (body.kind() != AgeSkyProfile.Kind.SUN) continue;
|
||||||
|
if (++seenSuns <= 2) continue;
|
||||||
|
int points = 1000 * (seenSuns - 2);
|
||||||
|
contributions.add(new Contribution("extra_sun", -1, points,
|
||||||
|
"Sun " + seenSuns + " adds growing instability"));
|
||||||
|
}
|
||||||
return new AgeInstability(CURRENT_VERSION,
|
return new AgeInstability(CURRENT_VERSION,
|
||||||
contributions.stream().mapToInt(Contribution::points).sum(), contributions);
|
contributions.stream().mapToInt(Contribution::points).sum(), contributions);
|
||||||
}
|
}
|
||||||
@@ -94,7 +109,8 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
|||||||
if (cave) {
|
if (cave) {
|
||||||
var role = symbol.semantics().role();
|
var role = symbol.semantics().role();
|
||||||
if (switch (role) {
|
if (switch (role) {
|
||||||
case WEATHER, SEASON, CELESTIAL, PERIOD_MODIFIER, PHASE_MODIFIER, ANGLE_MODIFIER -> true;
|
case WEATHER, SEASON, CELESTIAL, PERIOD_MODIFIER, PHASE_MODIFIER,
|
||||||
|
ANGLE_MODIFIER, DIRECTION_MODIFIER -> true;
|
||||||
default -> false;
|
default -> false;
|
||||||
}) {
|
}) {
|
||||||
contributions.add(new Contribution("cave_ignored_" + role.id(), symbol.index(),
|
contributions.add(new Contribution("cave_ignored_" + role.id(), symbol.index(),
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ public final class AgeRegistrySavedData extends SavedData {
|
|||||||
AgeTraits effectiveTraits = plan == null
|
AgeTraits effectiveTraits = plan == null
|
||||||
? AgeEnvironmentCompiler.forTerrain(traits, AgeRuntimeProfiles.resolve(seedProfile))
|
? AgeEnvironmentCompiler.forTerrain(traits, AgeRuntimeProfiles.resolve(seedProfile))
|
||||||
: AgeEnvironmentCompiler.apply(plan, traits);
|
: AgeEnvironmentCompiler.apply(plan, traits);
|
||||||
AgeInstability instability = AgeInstability.compute(plan, effectiveTraits, registries);
|
AgeInstability instability = AgeInstability.compute(plan, effectiveTraits, registries, seed);
|
||||||
AgeInstabilityPlan instabilityPlan = AgeInstabilityPlan.select(instability, seed);
|
AgeInstabilityPlan instabilityPlan = AgeInstabilityPlan.select(instability, seed);
|
||||||
AgeDefinition created = new AgeDefinition(
|
AgeDefinition created = new AgeDefinition(
|
||||||
reference.id(),
|
reference.id(),
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package net.dnorberg.rivencraft.book;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
|
|
||||||
|
/** The ordered, deterministic sky specification for an Age. No client data is trusted here. */
|
||||||
|
public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
|
||||||
|
public static final int RANDOM_SKY_GENERATION_VERSION = 17;
|
||||||
|
private static final int[] COLORS = {0xB74A50, 0xD9834B, 0xDBC574, 0x5AAB80,
|
||||||
|
0x6FA8DF, 0x9870B9, 0xD5DBE1, 0x252531};
|
||||||
|
|
||||||
|
public AgeSkyProfile {
|
||||||
|
bodies = List.copyOf(bodies);
|
||||||
|
long suns = bodies.stream().filter(body -> body.kind() == Kind.SUN).count();
|
||||||
|
long moons = bodies.size() - suns;
|
||||||
|
if (suns > 10 || moons > 10) throw new IllegalArgumentException("An Age may have at most ten suns and ten moons");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AgeSkyProfile forAge(AgeDefinition age) {
|
||||||
|
if (age.plan() != null) return compile(age.plan(), age.traits());
|
||||||
|
if (age.generationVersion() >= RANDOM_SKY_GENERATION_VERSION)
|
||||||
|
return random(age.seed(), age.traits());
|
||||||
|
return new AgeSkyProfile(Style.INHERITED, -1, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AgeSkyProfile random(long seed, AgeTraits traits) {
|
||||||
|
Random random = new Random(seed ^ 0x534B5950524F464CL);
|
||||||
|
Style style = Style.values()[random.nextInt(Style.values().length)];
|
||||||
|
int color = randomColor(random);
|
||||||
|
int suns = random.nextInt(4);
|
||||||
|
int moons = random.nextInt(4);
|
||||||
|
List<Body> bodies = new ArrayList<>();
|
||||||
|
for (int i = 0; i < suns; i++)
|
||||||
|
bodies.add(new Body(Kind.SUN, randomColor(random),
|
||||||
|
0.75F + random.nextFloat(), random.nextFloat() * 360.0F,
|
||||||
|
traits.celestialPeriod(), random.nextFloat(), random.nextBoolean()));
|
||||||
|
for (int i = 0; i < moons; i++)
|
||||||
|
bodies.add(new Body(Kind.MOON, randomColor(random),
|
||||||
|
0.75F + random.nextFloat(), random.nextFloat() * 360.0F,
|
||||||
|
traits.celestialPeriod(), random.nextFloat(), random.nextBoolean()));
|
||||||
|
return new AgeSkyProfile(style, color, 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);
|
||||||
|
boolean customBodies = hasCustomCelestials(plan);
|
||||||
|
List<Body> bodies = new ArrayList<>();
|
||||||
|
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
|
||||||
|
AgeTraits seeded = AgeTraits.select(plan.ageSeed());
|
||||||
|
double period = 1.0D;
|
||||||
|
float angle = seeded.celestialAngle();
|
||||||
|
float phase = seeded.celestialPhase();
|
||||||
|
float size = 1.0F;
|
||||||
|
List<Integer> bodyColors = new ArrayList<>();
|
||||||
|
boolean clockwise = true;
|
||||||
|
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||||
|
if (!active.contains(symbol.index())) continue;
|
||||||
|
SymbolSemantics semantics = symbol.semantics();
|
||||||
|
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 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);
|
||||||
|
case SIZE_MODIFIER -> size = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||||
|
case CELESTIAL_COLOR -> bodyColors.add(color(
|
||||||
|
((SymbolSemantics.ChoiceValue) semantics.value()).id()));
|
||||||
|
case DIRECTION_MODIFIER -> clockwise = ((SymbolSemantics.ChoiceValue) semantics.value())
|
||||||
|
.id().equals("clockwise");
|
||||||
|
case CELESTIAL -> {
|
||||||
|
String choice = ((SymbolSemantics.ChoiceValue) semantics.value()).id();
|
||||||
|
int bodyColor = bodyColors.isEmpty() ? 0xFFFFFF : mixColors(bodyColors);
|
||||||
|
if (choice.equals("normal_time")) {
|
||||||
|
if (customBodies) {
|
||||||
|
bodies.add(new Body(Kind.SUN, bodyColor, size, angle, period, phase, clockwise));
|
||||||
|
bodies.add(new Body(Kind.MOON, bodyColor, 1.0F, angle, period,
|
||||||
|
(phase + 0.5F) % 1.0F, clockwise));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodies.add(new Body(choice.equals("sun") ? Kind.SUN : Kind.MOON,
|
||||||
|
bodyColor, size, angle, period, phase, clockwise));
|
||||||
|
}
|
||||||
|
period = 1.0D;
|
||||||
|
angle = seeded.celestialAngle();
|
||||||
|
phase = seeded.celestialPhase();
|
||||||
|
size = 1.0F;
|
||||||
|
bodyColors.clear();
|
||||||
|
clockwise = true;
|
||||||
|
}
|
||||||
|
default -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Missing branches are seed-completed, including the celestial branch.
|
||||||
|
// An explicit sky or sky-color page does not suppress that completion.
|
||||||
|
if (plan.activeSymbols().stream().noneMatch(symbol ->
|
||||||
|
symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL)) {
|
||||||
|
bodies.addAll(random(plan.ageSeed(), seeded).bodies());
|
||||||
|
}
|
||||||
|
return new AgeSkyProfile(style, color, bodies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int mixColors(List<Integer> colors) {
|
||||||
|
if (colors.isEmpty()) return -1;
|
||||||
|
long red = 0, green = 0, blue = 0;
|
||||||
|
for (int color : colors) {
|
||||||
|
red += color >> 16 & 255;
|
||||||
|
green += color >> 8 & 255;
|
||||||
|
blue += color & 255;
|
||||||
|
}
|
||||||
|
int count = colors.size();
|
||||||
|
return (int) ((red + count / 2) / count) << 16
|
||||||
|
| (int) ((green + count / 2) / count) << 8
|
||||||
|
| (int) ((blue + count / 2) / count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int randomColor(Random random) {
|
||||||
|
int count = 1 + random.nextInt(3);
|
||||||
|
List<Integer> colors = new ArrayList<>(count);
|
||||||
|
for (int i = 0; i < count; i++) colors.add(COLORS[random.nextInt(COLORS.length)]);
|
||||||
|
return mixColors(colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasCustomCelestials(AgeGenerationPlan plan) {
|
||||||
|
return plan != null && 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean custom() {
|
||||||
|
return style == Style.STATIC || color != -1 || !bodies.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vanilla has one skylight channel; combine visible bodies into that channel. */
|
||||||
|
public float lightAt(double dayTime) {
|
||||||
|
float sunlight = 0.0F;
|
||||||
|
float moonlight = 0.0F;
|
||||||
|
for (Body body : bodies) {
|
||||||
|
double orbit = body.period() == 0.0D ? body.phase()
|
||||||
|
: (body.clockwise() ? 1.0D : -1.0D) * dayTime
|
||||||
|
/ (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;
|
||||||
|
else moonlight += altitude * 0.18F;
|
||||||
|
}
|
||||||
|
return Math.min(1.0F, sunlight > 0.0F ? sunlight : moonlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int color(String name) {
|
||||||
|
return switch (name) {
|
||||||
|
case "red" -> COLORS[0];
|
||||||
|
case "orange" -> COLORS[1];
|
||||||
|
case "yellow" -> COLORS[2];
|
||||||
|
case "green" -> COLORS[3];
|
||||||
|
case "blue" -> COLORS[4];
|
||||||
|
case "purple" -> COLORS[5];
|
||||||
|
case "white" -> COLORS[6];
|
||||||
|
case "black" -> COLORS[7];
|
||||||
|
default -> throw new IllegalArgumentException("Unknown sky color " + name);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Style { INHERITED, OVERWORLD, NETHER, END, STATIC }
|
||||||
|
public enum Kind { SUN, MOON }
|
||||||
|
public record Body(Kind kind, int color, float size, float angle,
|
||||||
|
double period, float phase, boolean clockwise) { }
|
||||||
|
}
|
||||||
@@ -52,9 +52,11 @@ public final class EnvironmentWritingGameTests {
|
|||||||
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
|
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
|
||||||
Set<String> expected = Set.of(
|
Set<String> expected = Set.of(
|
||||||
"lighting/normal", "lighting/dark", "lighting/bright",
|
"lighting/normal", "lighting/dark", "lighting/bright",
|
||||||
"sky/overworld", "sky/nether", "sky/end",
|
"sky/overworld", "sky/nether", "sky/end", "sky/static", "sky_color/purple",
|
||||||
"weather/normal", "weather/fast", "weather/slow", "weather/off",
|
"weather/normal", "weather/fast", "weather/slow", "weather/off",
|
||||||
"weather/always", "weather/storm", "celestial/normal_time",
|
"weather/always", "weather/storm", "celestial/normal_time", "celestial/sun",
|
||||||
|
"celestial/moon", "celestial_color/red", "size/large", "angle/east",
|
||||||
|
"direction/clockwise", "direction/counterclockwise",
|
||||||
"season/no_seasons", "season/seasons", "season/eternal_winter",
|
"season/no_seasons", "season/seasons", "season/eternal_winter",
|
||||||
"season/eternal_summer", "season/eternal_fall", "season/eternal_spring",
|
"season/eternal_summer", "season/eternal_fall", "season/eternal_spring",
|
||||||
"period/zero", "period/half", "period/full", "period/double",
|
"period/zero", "period/half", "period/full", "period/double",
|
||||||
@@ -135,6 +137,104 @@ public final class EnvironmentWritingGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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")));
|
||||||
|
AgeTraits seeded = AgeTraits.select(44L);
|
||||||
|
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 44L, AgeGenerationProfiles.STANDARD,
|
||||||
|
seeded, helper.getLevel().registryAccess());
|
||||||
|
helper.assertTrue(AgeEnvironmentCompiler.staticSky(plan),
|
||||||
|
"The authored Static Sky page must survive Age compilation");
|
||||||
|
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(plan, BuiltinDimensionTypes.OVERWORLD_EFFECTS),
|
||||||
|
AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS,
|
||||||
|
"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");
|
||||||
|
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,
|
||||||
|
"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"))),
|
||||||
|
45L, AgeGenerationProfiles.STANDARD, AgeTraits.select(45L),
|
||||||
|
helper.getLevel().registryAccess());
|
||||||
|
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(coloredEnd,
|
||||||
|
BuiltinDimensionTypes.OVERWORLD_EFFECTS),
|
||||||
|
AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS,
|
||||||
|
"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");
|
||||||
|
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("sky/static")
|
||||||
|
&& PageItem.bundledCreativeSymbols().contains("sky_color/purple"),
|
||||||
|
"New sky pages must be available through the Creative and JEI page catalog");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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")));
|
||||||
|
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")));
|
||||||
|
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), skyColor,
|
||||||
|
"Sky color pages must blend without discarding either page");
|
||||||
|
AgeSkyProfile profile = AgeSkyProfile.compile(plan, traits);
|
||||||
|
helper.assertValueEqual(profile.bodies().size(), 1, "The written Age has exactly one sun");
|
||||||
|
helper.assertValueEqual(profile.bodies().getFirst().color(), sunColor,
|
||||||
|
"Celestial colors must blend for the following body");
|
||||||
|
helper.assertValueEqual(AgeSkyProfile.compile(AgeGenerationPlan.load(plan.save()), traits), profile,
|
||||||
|
"Blended colors must survive the saved writing plan");
|
||||||
|
|
||||||
|
AgeSkyProfile bodyless = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, skyColor, List.of());
|
||||||
|
helper.assertTrue(bodyless.custom() && bodyless.bodies().isEmpty(),
|
||||||
|
"An Age can have an authored sky without any celestial bodies");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void randomAndAuthoredCelestialsKeepIndependentOrbits(GameTestHelper helper) {
|
||||||
|
AgeTraits traits = AgeTraits.select(12345L);
|
||||||
|
AgeSkyProfile first = AgeSkyProfile.random(12345L, traits);
|
||||||
|
helper.assertValueEqual(first, AgeSkyProfile.random(12345L, traits),
|
||||||
|
"A random Age must recreate the same sky from its saved seed");
|
||||||
|
helper.assertTrue(first.bodies().stream().filter(body -> body.kind() == AgeSkyProfile.Kind.SUN).count() <= 10
|
||||||
|
&& first.bodies().stream().filter(body -> body.kind() == AgeSkyProfile.Kind.MOON).count() <= 10,
|
||||||
|
"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("size/large"), page("period/half"), page("celestial/sun"),
|
||||||
|
page("direction/clockwise"), page("celestial/moon")));
|
||||||
|
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 12345L, AgeGenerationProfiles.STANDARD,
|
||||||
|
traits, helper.getLevel().registryAccess());
|
||||||
|
AgeSkyProfile sky = AgeSkyProfile.compile(plan, AgeEnvironmentCompiler.apply(plan, traits));
|
||||||
|
helper.assertValueEqual(sky.bodies().size(), 2, "Both written celestial bodies must be retained");
|
||||||
|
helper.assertValueEqual(sky.bodies().getFirst().angle(), 90.0F,
|
||||||
|
"The first body's compass angle must be independent");
|
||||||
|
helper.assertFalse(sky.bodies().getFirst().clockwise(),
|
||||||
|
"The first body should move counterclockwise");
|
||||||
|
helper.assertTrue(sky.bodies().getLast().clockwise(),
|
||||||
|
"The second body should move clockwise");
|
||||||
|
helper.assertValueEqual(AgeSkyProfile.compile(AgeGenerationPlan.load(plan.save()), traits), sky,
|
||||||
|
"Body angle and direction must survive Age-plan serialization");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void writtenEnvironmentPersistsAndPageFreeAgesStaySeeded(GameTestHelper helper) {
|
public static void writtenEnvironmentPersistsAndPageFreeAgesStaySeeded(GameTestHelper helper) {
|
||||||
OrderedWriting writing = new OrderedWriting(List.of(
|
OrderedWriting writing = new OrderedWriting(List.of(
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package net.dnorberg.rivencraft.book;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.mixin.MobDropChanceAccessor;
|
||||||
|
import net.minecraft.world.Container;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.EquipmentSlot;
|
||||||
|
import net.minecraft.world.entity.Mob;
|
||||||
|
import net.minecraft.world.entity.animal.horse.AbstractHorse;
|
||||||
|
import net.minecraft.world.entity.item.ItemEntity;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
/** Legacy-style Disarm: leave carried items and eligible equipment at the departure point. */
|
||||||
|
public final class LinkDisarmService {
|
||||||
|
private LinkDisarmService() {}
|
||||||
|
|
||||||
|
public static boolean mayTravel(Entity entity) {
|
||||||
|
// Legacy refuses loose items and dropped Link Books on Disarm links.
|
||||||
|
return !(entity instanceof ItemEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void disarm(Entity entity) {
|
||||||
|
if (entity instanceof ServerPlayer player) {
|
||||||
|
eject(player, player.getInventory());
|
||||||
|
player.containerMenu.broadcastChanges();
|
||||||
|
}
|
||||||
|
if (entity instanceof Container container) eject(entity, container);
|
||||||
|
if (entity instanceof AbstractHorse horse) eject(entity, horse.getInventory());
|
||||||
|
if (entity instanceof Mob mob) dropEquipment(mob);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void eject(Entity carrier, Container inventory) {
|
||||||
|
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
|
||||||
|
ItemStack stack = inventory.getItem(slot);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
inventory.setItem(slot, ItemStack.EMPTY);
|
||||||
|
carrier.spawnAtLocation(stack);
|
||||||
|
}
|
||||||
|
inventory.setChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void dropEquipment(Mob mob) {
|
||||||
|
for (EquipmentSlot slot : EquipmentSlot.values()) {
|
||||||
|
ItemStack stack = mob.getItemBySlot(slot);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
float chance = ((MobDropChanceAccessor) mob).rivencraft$getEquipmentDropChance(slot);
|
||||||
|
mob.setItemSlot(slot, ItemStack.EMPTY);
|
||||||
|
if (chance <= 0 || mob.getRandom().nextFloat() >= chance) continue;
|
||||||
|
if (chance <= 1 && stack.isDamageableItem()) {
|
||||||
|
int minimum = Math.max(stack.getMaxDamage() - 25, 1);
|
||||||
|
int damage = stack.getMaxDamage() - mob.getRandom().nextInt(
|
||||||
|
1 + mob.getRandom().nextInt(minimum));
|
||||||
|
stack.setDamageValue(Math.clamp(damage, 1, minimum));
|
||||||
|
}
|
||||||
|
mob.spawnAtLocation(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package net.dnorberg.rivencraft.book;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.GlobalPos;
|
||||||
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.world.entity.EquipmentSlot;
|
||||||
|
import net.minecraft.world.entity.EntityType;
|
||||||
|
import net.minecraft.world.entity.animal.horse.Donkey;
|
||||||
|
import net.minecraft.world.entity.item.ItemEntity;
|
||||||
|
import net.minecraft.world.entity.monster.Zombie;
|
||||||
|
import net.minecraft.world.entity.vehicle.MinecartChest;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
|
@GameTestHolder(Rivencraft.MOD_ID)
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class LinkModifierBehaviorGameTests {
|
||||||
|
private LinkModifierBehaviorGameTests() {}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void maintainMomentumRotatesToDestinationYaw(GameTestHelper helper) {
|
||||||
|
Vec3 result = LinkingService.rotatedMomentum(new Vec3(3, -0.5, 4), 90);
|
||||||
|
helper.assertTrue(Math.abs(result.x) < 0.0001 && Math.abs(result.y + 0.3) < 0.0001
|
||||||
|
&& Math.abs(result.z - 5) < 0.0001,
|
||||||
|
"Legacy momentum preserves horizontal speed, rotates to destination yaw, and adds a small lift");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void disarmRefusesLooseItems(GameTestHelper helper) {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
ItemEntity item = new ItemEntity(level, 1, 1, 1, new ItemStack(Items.DIAMOND));
|
||||||
|
level.addFreshEntity(item);
|
||||||
|
LinkTarget target = new LinkTarget(GlobalPos.of(level.dimension(), helper.absolutePos(new BlockPos(2, 1, 2))),
|
||||||
|
0, 0);
|
||||||
|
helper.assertValueEqual(LinkingService.travelEntityPortal(item, target,
|
||||||
|
new LinkProperties(LinkProperties.DISARM | LinkProperties.INTRA_LINKING), false),
|
||||||
|
LinkingService.LinkResult.DISARM_REJECTED,
|
||||||
|
"Legacy Disarm must refuse a loose item before teleportation");
|
||||||
|
helper.assertTrue(item.isAlive() && item.getItem().is(Items.DIAMOND),
|
||||||
|
"Rejected item must remain at the source");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void ordinaryLinkMovesLooseItemWithoutMakingPlatform(GameTestHelper helper) {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
BlockPos landing = helper.absolutePos(new BlockPos(2, 2, 2));
|
||||||
|
level.setBlock(landing.below(), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(landing, Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(landing.above(), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
ItemEntity item = new ItemEntity(level, 1, 1, 1, new ItemStack(Items.DIAMOND));
|
||||||
|
level.addFreshEntity(item);
|
||||||
|
LinkTarget target = new LinkTarget(GlobalPos.of(level.dimension(), landing), 0, 0);
|
||||||
|
helper.assertValueEqual(LinkingService.travelEntityPortal(item, target,
|
||||||
|
new LinkProperties(LinkProperties.INTRA_LINKING), false),
|
||||||
|
LinkingService.LinkResult.SUCCESS, "Ordinary portal links may carry loose items");
|
||||||
|
helper.assertTrue(item.blockPosition().distManhattan(landing) <= 1,
|
||||||
|
"Item must arrive at the linked location");
|
||||||
|
helper.assertTrue(level.getBlockState(landing.below()).isAir(),
|
||||||
|
"A Linking Book must not generate a landing platform by default");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void explicitGeneratePlatformAddsOnlyArrivalFloor(GameTestHelper helper) {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
BlockPos landing = helper.absolutePos(new BlockPos(2, 2, 2));
|
||||||
|
level.setBlock(landing.below(), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(landing, Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(landing.above(), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(landing.above(2), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
ItemEntity item = new ItemEntity(level, 1, 1, 1, new ItemStack(Items.DIAMOND));
|
||||||
|
level.addFreshEntity(item);
|
||||||
|
LinkTarget target = new LinkTarget(GlobalPos.of(level.dimension(), landing), 0, 0);
|
||||||
|
helper.assertValueEqual(LinkingService.travelEntityPortal(item, target,
|
||||||
|
new LinkProperties(LinkProperties.INTRA_LINKING | LinkProperties.GENERATE_PLATFORM), false),
|
||||||
|
LinkingService.LinkResult.SUCCESS, "An explicit platform link must travel");
|
||||||
|
helper.assertTrue(level.getBlockState(landing.below()).is(
|
||||||
|
net.dnorberg.rivencraft.registry.ModBlocks.CLOUD_PLATFORM.get()),
|
||||||
|
"An enabled Linking Book platform must add one cloud block below the arrival");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void disarmEmptiesCarrierAndMobEquipment(GameTestHelper helper) {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
Vec3 source = Vec3.atCenterOf(helper.absolutePos(new BlockPos(1, 1, 1)));
|
||||||
|
MinecartChest cart = EntityType.CHEST_MINECART.create(level);
|
||||||
|
Donkey donkey = EntityType.DONKEY.create(level);
|
||||||
|
Zombie zombie = EntityType.ZOMBIE.create(level);
|
||||||
|
helper.assertTrue(cart != null && donkey != null && zombie != null, "Carriers must instantiate");
|
||||||
|
cart.setPos(source);
|
||||||
|
donkey.setPos(source);
|
||||||
|
zombie.setPos(source);
|
||||||
|
level.addFreshEntity(cart);
|
||||||
|
level.addFreshEntity(donkey);
|
||||||
|
level.addFreshEntity(zombie);
|
||||||
|
cart.setItem(0, new ItemStack(Items.EMERALD));
|
||||||
|
donkey.getInventory().setItem(0, new ItemStack(Items.SADDLE));
|
||||||
|
zombie.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_SWORD));
|
||||||
|
zombie.setDropChance(EquipmentSlot.MAINHAND, 2.0F);
|
||||||
|
|
||||||
|
LinkDisarmService.disarm(cart);
|
||||||
|
LinkDisarmService.disarm(donkey);
|
||||||
|
LinkDisarmService.disarm(zombie);
|
||||||
|
|
||||||
|
helper.assertTrue(cart.getItem(0).isEmpty() && donkey.getInventory().getItem(0).isEmpty()
|
||||||
|
&& zombie.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty(),
|
||||||
|
"Disarm must remove inventory contents and worn equipment");
|
||||||
|
var drops = level.getEntitiesOfClass(ItemEntity.class, new AABB(source, source).inflate(3));
|
||||||
|
helper.assertTrue(drops.stream().anyMatch(item -> item.getItem().is(Items.EMERALD))
|
||||||
|
&& drops.stream().anyMatch(item -> item.getItem().is(Items.SADDLE))
|
||||||
|
&& drops.stream().anyMatch(item -> item.getItem().is(Items.IRON_SWORD)),
|
||||||
|
"Carrier items and guaranteed-drop mob equipment must remain at departure");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import net.minecraft.network.chat.Component;
|
|||||||
/** Flags written into a Link Panel and carried by books assembled from it. */
|
/** Flags written into a Link Panel and carried by books assembled from it. */
|
||||||
public record LinkProperties(int flags) {
|
public record LinkProperties(int flags) {
|
||||||
public static final int DISARM = 1;
|
public static final int DISARM = 1;
|
||||||
/** Retained to read older test books; a newly edited book uses DISABLE_PLATFORM instead. */
|
/** Linking Books do not create a platform unless this Legacy flag is enabled. */
|
||||||
public static final int GENERATE_PLATFORM = 2;
|
public static final int GENERATE_PLATFORM = 2;
|
||||||
public static final int MAINTAIN_MOMENTUM = 4;
|
public static final int MAINTAIN_MOMENTUM = 4;
|
||||||
public static final int INTRA_LINKING = 8;
|
public static final int INTRA_LINKING = 8;
|
||||||
@@ -48,6 +48,7 @@ public record LinkProperties(int flags) {
|
|||||||
if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking"));
|
if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking"));
|
||||||
if (has(INTRA_LINKING_ONLY)) lines.add(propertyName("intra_linking_only"));
|
if (has(INTRA_LINKING_ONLY)) lines.add(propertyName("intra_linking_only"));
|
||||||
if (has(RELATIVE_LINK)) lines.add(propertyName("relative_link"));
|
if (has(RELATIVE_LINK)) lines.add(propertyName("relative_link"));
|
||||||
|
if (has(GENERATE_PLATFORM)) lines.add(propertyName("generate_platform"));
|
||||||
if (has(DISABLE_PLATFORM)) lines.add(propertyName("disable_platform"));
|
if (has(DISABLE_PLATFORM)) lines.add(propertyName("disable_platform"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ public record LinkProperties(int flags) {
|
|||||||
case INTRA_LINKING -> propertyName("intra_linking");
|
case INTRA_LINKING -> propertyName("intra_linking");
|
||||||
case INTRA_LINKING_ONLY -> propertyName("intra_linking_only");
|
case INTRA_LINKING_ONLY -> propertyName("intra_linking_only");
|
||||||
case RELATIVE_LINK -> propertyName("relative_link");
|
case RELATIVE_LINK -> propertyName("relative_link");
|
||||||
|
case GENERATE_PLATFORM -> propertyName("generate_platform");
|
||||||
case DISABLE_PLATFORM -> propertyName("disable_platform");
|
case DISABLE_PLATFORM -> propertyName("disable_platform");
|
||||||
default -> throw new IllegalArgumentException("Unknown link property flag: " + flag);
|
default -> throw new IllegalArgumentException("Unknown link property flag: " + flag);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,16 +6,20 @@ import java.util.stream.IntStream;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
||||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||||
|
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.Direction;
|
import net.minecraft.core.Direction;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.tags.BlockTags;
|
import net.minecraft.tags.BlockTags;
|
||||||
import net.minecraft.world.level.border.WorldBorder;
|
import net.minecraft.world.level.border.WorldBorder;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraft.world.level.portal.DimensionTransition;
|
||||||
import net.neoforged.fml.ModList;
|
import net.neoforged.fml.ModList;
|
||||||
|
|
||||||
/** Server-side validation and execution for book travel. */
|
/** Server-side validation and execution for book travel. */
|
||||||
@@ -96,9 +100,9 @@ public final class LinkingService {
|
|||||||
|
|
||||||
Vec3 position = destination.get();
|
Vec3 position = destination.get();
|
||||||
Vec3 momentum = player.getDeltaMovement();
|
Vec3 momentum = player.getDeltaMovement();
|
||||||
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
|
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(player);
|
||||||
teleport(player, destinationLevel, position, target, true);
|
teleport(player, destinationLevel, position, target, true);
|
||||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
|
applyMomentum(player, momentum, target, properties);
|
||||||
player.resetFallDistance();
|
player.resetFallDistance();
|
||||||
return LinkResult.SUCCESS;
|
return LinkResult.SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -114,9 +118,9 @@ public final class LinkingService {
|
|||||||
landingOrigin(player, destinationLevel, target, properties), true);
|
landingOrigin(player, destinationLevel, target, properties), true);
|
||||||
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
|
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
|
||||||
Vec3 momentum = player.getDeltaMovement();
|
Vec3 momentum = player.getDeltaMovement();
|
||||||
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
|
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(player);
|
||||||
teleport(player, destinationLevel, destination.get(), target, instant);
|
teleport(player, destinationLevel, destination.get(), target, instant);
|
||||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
|
applyMomentum(player, momentum, target, properties);
|
||||||
player.resetFallDistance();
|
player.resetFallDistance();
|
||||||
return LinkResult.SUCCESS;
|
return LinkResult.SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -142,36 +146,80 @@ public final class LinkingService {
|
|||||||
|
|
||||||
Vec3 position = destination.get();
|
Vec3 position = destination.get();
|
||||||
Vec3 momentum = player.getDeltaMovement();
|
Vec3 momentum = player.getDeltaMovement();
|
||||||
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
|
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(player);
|
||||||
teleport(player, destinationLevel, position, target, instant);
|
teleport(player, destinationLevel, position, target, instant);
|
||||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
|
if (properties.has(LinkProperties.GENERATE_PLATFORM))
|
||||||
|
generateArrivalPlatform(destinationLevel, position);
|
||||||
|
applyMomentum(player, momentum, target, properties);
|
||||||
player.resetFallDistance();
|
player.resetFallDistance();
|
||||||
return LinkResult.SUCCESS;
|
return LinkResult.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Non-player portal travel follows the same link rules; Disarm may remove carried cargo. */
|
||||||
|
public static LinkResult travelEntityPortal(Entity entity, LinkTarget target,
|
||||||
|
LinkProperties properties, boolean requiresFooting) {
|
||||||
|
if (!(entity.level() instanceof ServerLevel source)) return LinkResult.MISSING_DIMENSION;
|
||||||
|
ServerLevel destinationLevel = source.getServer().getLevel(target.location().dimension());
|
||||||
|
if (destinationLevel == null) return LinkResult.MISSING_DIMENSION;
|
||||||
|
if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != source)
|
||||||
|
return LinkResult.INTRA_ONLY;
|
||||||
|
if (destinationLevel == source && !properties.has(LinkProperties.INTRA_LINKING)
|
||||||
|
&& !properties.has(LinkProperties.INTRA_LINKING_ONLY)) return LinkResult.INTRA_LINKING_REQUIRED;
|
||||||
|
if (properties.has(LinkProperties.DISARM) && !LinkDisarmService.mayTravel(entity))
|
||||||
|
return LinkResult.DISARM_REJECTED;
|
||||||
|
if (!entity.canChangeDimensions(source, destinationLevel)) return LinkResult.NO_SAFE_DESTINATION;
|
||||||
|
BlockPos origin = landingOrigin(entity, destinationLevel, target, properties);
|
||||||
|
Optional<Vec3> destination = resolveDestination(destinationLevel, origin, requiresFooting);
|
||||||
|
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
|
||||||
|
|
||||||
|
Vec3 momentum = entity.getDeltaMovement();
|
||||||
|
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(entity);
|
||||||
|
Entity arrived = entity.changeDimension(new DimensionTransition(destinationLevel, destination.get(),
|
||||||
|
properties.has(LinkProperties.MAINTAIN_MOMENTUM)
|
||||||
|
? rotatedMomentum(momentum, target.yaw()) : Vec3.ZERO,
|
||||||
|
target.yaw(), target.pitch(), DimensionTransition.DO_NOTHING));
|
||||||
|
if (arrived == null) return LinkResult.NO_SAFE_DESTINATION;
|
||||||
|
if (!requiresFooting && properties.has(LinkProperties.GENERATE_PLATFORM))
|
||||||
|
generateArrivalPlatform(destinationLevel, destination.get());
|
||||||
|
arrived.setXRot(target.pitch());
|
||||||
|
arrived.resetFallDistance();
|
||||||
|
PortalTravelCooldown.mark(arrived, source.getServer().getTickCount());
|
||||||
|
return LinkResult.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vec3 rotatedMomentum(Vec3 incoming, float destinationYaw) {
|
||||||
|
// Legacy rotates the horizontal speed to the destination yaw and adds a small upward lift.
|
||||||
|
double horizontalSpeed = Math.hypot(incoming.x, incoming.z);
|
||||||
|
double radians = Math.toRadians(destinationYaw);
|
||||||
|
return new Vec3(horizontalSpeed * Math.cos(radians), incoming.y + 0.2,
|
||||||
|
horizontalSpeed * Math.sin(radians));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyMomentum(ServerPlayer player, Vec3 incoming, LinkTarget target,
|
||||||
|
LinkProperties properties) {
|
||||||
|
player.setDeltaMovement(properties.has(LinkProperties.MAINTAIN_MOMENTUM)
|
||||||
|
? rotatedMomentum(incoming, target.yaw()) : Vec3.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generateArrivalPlatform(ServerLevel level, Vec3 arrival) {
|
||||||
|
BlockPos feet = BlockPos.containing(arrival);
|
||||||
|
BlockPos floor = feet.below();
|
||||||
|
// Legacy only fills a missing floor when there is clear space above the arrival.
|
||||||
|
if (level.isEmptyBlock(floor) && level.isEmptyBlock(feet.above(2)))
|
||||||
|
level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
/** Legacy relative links retain the traveller's offset from the source spawn. */
|
/** Legacy relative links retain the traveller's offset from the source spawn. */
|
||||||
private static BlockPos landingOrigin(ServerPlayer player, ServerLevel destinationLevel,
|
private static BlockPos landingOrigin(Entity player, ServerLevel destinationLevel,
|
||||||
LinkTarget target, LinkProperties properties) {
|
LinkTarget target, LinkProperties properties) {
|
||||||
if (!properties.has(LinkProperties.RELATIVE_LINK)) return target.location().pos();
|
if (!properties.has(LinkProperties.RELATIVE_LINK)) return target.location().pos();
|
||||||
BlockPos sourceSpawn = player.serverLevel().getSharedSpawnPos();
|
BlockPos sourceSpawn = ((ServerLevel) player.level()).getSharedSpawnPos();
|
||||||
BlockPos destinationSpawn = destinationLevel.getSharedSpawnPos();
|
BlockPos destinationSpawn = destinationLevel.getSharedSpawnPos();
|
||||||
BlockPos departure = player.blockPosition();
|
BlockPos departure = player.blockPosition();
|
||||||
return destinationSpawn.offset(departure.getX() - sourceSpawn.getX(),
|
return destinationSpawn.offset(departure.getX() - sourceSpawn.getX(),
|
||||||
departure.getY() - sourceSpawn.getY(), departure.getZ() - sourceSpawn.getZ());
|
departure.getY() - sourceSpawn.getY(), departure.getZ() - sourceSpawn.getZ());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drop each inventory stack as an item entity at the departure point. */
|
|
||||||
private static void dropInventoryAtSource(ServerPlayer player) {
|
|
||||||
var inventory = player.getInventory();
|
|
||||||
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
|
|
||||||
var stack = inventory.getItem(slot);
|
|
||||||
if (stack.isEmpty()) continue;
|
|
||||||
inventory.setItem(slot, net.minecraft.world.item.ItemStack.EMPTY);
|
|
||||||
player.drop(stack, false);
|
|
||||||
}
|
|
||||||
player.containerMenu.broadcastChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void teleport(
|
private static void teleport(
|
||||||
ServerPlayer player,
|
ServerPlayer player,
|
||||||
ServerLevel destinationLevel,
|
ServerLevel destinationLevel,
|
||||||
@@ -285,6 +333,7 @@ public final class LinkingService {
|
|||||||
MISSING_DIMENSION,
|
MISSING_DIMENSION,
|
||||||
INTRA_LINKING_REQUIRED,
|
INTRA_LINKING_REQUIRED,
|
||||||
INTRA_ONLY,
|
INTRA_ONLY,
|
||||||
|
DISARM_REJECTED,
|
||||||
NO_SAFE_DESTINATION
|
NO_SAFE_DESTINATION
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package net.dnorberg.rivencraft.client;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
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.ModItems;
|
||||||
|
import net.minecraft.client.model.BookModel;
|
||||||
|
import net.minecraft.client.model.geom.ModelLayers;
|
||||||
|
import net.minecraft.client.renderer.MultiBufferSource;
|
||||||
|
import net.minecraft.client.renderer.RenderType;
|
||||||
|
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||||
|
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
/** Legacy's almost-closed book, oriented to the receptacle's mounting face. */
|
||||||
|
public final class BookReceptacleRenderer implements BlockEntityRenderer<BookReceptacleBlockEntity> {
|
||||||
|
private static final ResourceLocation LINKING_TEXTURE = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
"rivencraft", "textures/entity/linking_book.png");
|
||||||
|
private static final ResourceLocation DESCRIPTIVE_TEXTURE = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
"rivencraft", "textures/entity/descriptive_book.png");
|
||||||
|
private final BookModel model;
|
||||||
|
|
||||||
|
public BookReceptacleRenderer(BlockEntityRendererProvider.Context context) {
|
||||||
|
model = new BookModel(context.bakeLayer(ModelLayers.BOOK));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(BookReceptacleBlockEntity receptacle, float partialTick, PoseStack poseStack,
|
||||||
|
MultiBufferSource buffers, int packedLight, int packedOverlay) {
|
||||||
|
if (receptacle.getBook().isEmpty()) return;
|
||||||
|
|
||||||
|
poseStack.pushPose();
|
||||||
|
poseStack.translate(0.5, 0.5, 0.5);
|
||||||
|
Direction facing = receptacle.getBlockState().getValue(BookReceptacleBlock.FACING);
|
||||||
|
switch (facing) {
|
||||||
|
case UP -> {
|
||||||
|
// Rotate the whole assembled top-mounted receptacle/book pair
|
||||||
|
// around the block center, matching the x=90 block model.
|
||||||
|
poseStack.mulPose(Axis.ZP.rotationDegrees(180));
|
||||||
|
poseStack.mulPose(Axis.XP.rotationDegrees(-90));
|
||||||
|
poseStack.mulPose(Axis.YP.rotationDegrees(90));
|
||||||
|
// Preserve the local half-turn that seats the book in its slot.
|
||||||
|
poseStack.mulPose(Axis.ZP.rotationDegrees(180));
|
||||||
|
}
|
||||||
|
case DOWN -> {
|
||||||
|
// The ceiling-mounted model is the inverted top assembly.
|
||||||
|
poseStack.mulPose(Axis.XP.rotationDegrees(-90));
|
||||||
|
poseStack.mulPose(Axis.YP.rotationDegrees(90));
|
||||||
|
poseStack.mulPose(Axis.ZP.rotationDegrees(180));
|
||||||
|
}
|
||||||
|
case NORTH -> poseStack.mulPose(Axis.YP.rotationDegrees(-90));
|
||||||
|
case SOUTH -> poseStack.mulPose(Axis.YP.rotationDegrees(90));
|
||||||
|
case EAST -> poseStack.mulPose(Axis.YP.rotationDegrees(180));
|
||||||
|
case WEST -> { }
|
||||||
|
}
|
||||||
|
poseStack.scale(0.8F, 0.8F, 0.8F);
|
||||||
|
model.setupAnim(0.0F, 0.1F, 0.1F, 0.005F);
|
||||||
|
ResourceLocation texture = receptacle.getBook().is(ModItems.DESCRIPTIVE_BOOK.get())
|
||||||
|
? DESCRIPTIVE_TEXTURE : LINKING_TEXTURE;
|
||||||
|
VertexConsumer vertices = buffers.getBuffer(RenderType.entitySolid(texture));
|
||||||
|
model.render(poseStack, vertices, packedLight, packedOverlay, -1);
|
||||||
|
poseStack.popPose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,23 +2,58 @@ package net.dnorberg.rivencraft.client;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeSkyProfile;
|
||||||
import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
|
import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import net.dnorberg.rivencraft.network.RequestAgeSkyPayload;
|
||||||
|
|
||||||
/** Client cache used by both the active world and remote preview renderers. */
|
/** Client cache used by both the active world and remote preview renderers. */
|
||||||
public final class ClientAgeCelestialAngles {
|
public final class ClientAgeCelestialAngles {
|
||||||
private static final Map<ResourceKey<Level>, Float> ANGLES = new ConcurrentHashMap<>();
|
private static final Map<ResourceKey<Level>, Float> ANGLES = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<ResourceKey<Level>, AgeSkyProfile> SKIES = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<ResourceKey<Level>, Long> LAST_REQUEST = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private ClientAgeCelestialAngles() {
|
private ClientAgeCelestialAngles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void handle(AgeCelestialAnglePayload payload, IPayloadContext context) {
|
public static void handle(AgeCelestialAnglePayload payload, IPayloadContext context) {
|
||||||
ANGLES.put(payload.dimension(), payload.angle());
|
ANGLES.put(payload.dimension(), payload.angle());
|
||||||
|
SKIES.put(payload.dimension(), payload.sky());
|
||||||
|
LAST_REQUEST.remove(payload.dimension());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void requestIfMissing(ResourceKey<Level> dimension) {
|
||||||
|
if (!dimension.location().getNamespace().equals("rivencraft") || SKIES.containsKey(dimension)) return;
|
||||||
|
long now = System.nanoTime();
|
||||||
|
Long previous = LAST_REQUEST.putIfAbsent(dimension, now);
|
||||||
|
if (previous != null && now - previous < 2_000_000_000L) return;
|
||||||
|
LAST_REQUEST.put(dimension, now);
|
||||||
|
PacketDistributor.sendToServer(new RequestAgeSkyPayload(dimension));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
ANGLES.clear();
|
||||||
|
SKIES.clear();
|
||||||
|
LAST_REQUEST.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float get(ResourceKey<Level> dimension) {
|
public static float get(ResourceKey<Level> dimension) {
|
||||||
return ANGLES.getOrDefault(dimension, 0.0F);
|
return ANGLES.getOrDefault(dimension, 0.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int skyColor(ResourceKey<Level> dimension) {
|
||||||
|
return sky(dimension).color();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean staticSky(ResourceKey<Level> dimension) {
|
||||||
|
return sky(dimension).style() == AgeSkyProfile.Style.STATIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AgeSkyProfile sky(ResourceKey<Level> dimension) {
|
||||||
|
return SKIES.getOrDefault(dimension,
|
||||||
|
new AgeSkyProfile(AgeSkyProfile.Style.INHERITED, -1, java.util.List.of()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,12 @@ import net.neoforged.neoforge.common.NeoForge;
|
|||||||
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
|
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
|
||||||
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
||||||
import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent;
|
import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent;
|
||||||
|
import net.neoforged.neoforge.client.event.RegisterDimensionSpecialEffectsEvent;
|
||||||
|
import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent;
|
||||||
|
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeEnvironmentCompiler;
|
||||||
import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions;
|
import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions;
|
||||||
import net.neoforged.neoforge.client.extensions.common.RegisterClientExtensionsEvent;
|
import net.neoforged.neoforge.client.extensions.common.RegisterClientExtensionsEvent;
|
||||||
import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer;
|
import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer;
|
||||||
@@ -38,10 +44,28 @@ public final class ClientEvents {
|
|||||||
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
|
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
|
||||||
event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::new);
|
event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::new);
|
||||||
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
|
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
|
||||||
|
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_RECEPTACLE.get(), BookReceptacleRenderer::new);
|
||||||
event.registerBlockEntityRenderer(ModBlockEntities.WRITING_DESK.get(), WritingDeskRenderer::new);
|
event.registerBlockEntityRenderer(ModBlockEntities.WRITING_DESK.get(), WritingDeskRenderer::new);
|
||||||
event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new);
|
event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerSkyEffects(RegisterDimensionSpecialEffectsEvent event) {
|
||||||
|
event.register(AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS, new RivencraftSkyEffects());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerPortalColor(RegisterColorHandlersEvent.Block event) {
|
||||||
|
event.register((state, level, pos, tintIndex) -> {
|
||||||
|
if (level == null || pos == null || tintIndex != 0
|
||||||
|
|| !(level.getBlockEntity(pos) instanceof CrystalPortalBlockEntity portal)
|
||||||
|
|| portal.receptacle() == null
|
||||||
|
|| !(level.getBlockEntity(portal.receptacle()) instanceof BookReceptacleBlockEntity holder)
|
||||||
|
|| holder.destination() == null) return 0xFFFFFF;
|
||||||
|
return holder.skyColor();
|
||||||
|
}, ModBlocks.CRYSTAL_PORTAL.get());
|
||||||
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||||
event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new);
|
event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new);
|
||||||
@@ -98,6 +122,11 @@ public final class ClientEvents {
|
|||||||
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPost);
|
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPost);
|
||||||
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onRenderGui);
|
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onRenderGui);
|
||||||
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onMouseButton);
|
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onMouseButton);
|
||||||
|
NeoForge.EVENT_BUS.addListener(ClientEvents::onLogout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onLogout(ClientPlayerNetworkEvent.LoggingOut event) {
|
||||||
|
ClientAgeCelestialAngles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package net.dnorberg.rivencraft.client;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||||
|
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||||
|
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.blaze3d.vertex.Tesselator;
|
||||||
|
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||||
|
import java.util.Random;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeSkyProfile;
|
||||||
|
import net.minecraft.client.Camera;
|
||||||
|
import net.minecraft.client.multiplayer.ClientLevel;
|
||||||
|
import net.minecraft.client.renderer.DimensionSpecialEffects;
|
||||||
|
import net.minecraft.client.renderer.FogRenderer;
|
||||||
|
import net.minecraft.client.renderer.GameRenderer;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import com.mojang.math.Axis;
|
||||||
|
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 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");
|
||||||
|
|
||||||
|
public RivencraftSkyEffects() {
|
||||||
|
super(192.0F, true, SkyType.NORMAL, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vec3 getBrightnessDependentFogColor(Vec3 color, float brightness) {
|
||||||
|
return color.multiply(brightness * 0.94F + 0.06F,
|
||||||
|
brightness * 0.94F + 0.06F, brightness * 0.91F + 0.09F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFoggyAt(int x, int y) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean renderSky(ClientLevel level, int ticks, float partialTick, Matrix4f modelViewMatrix,
|
||||||
|
Camera camera, Matrix4f projectionMatrix, boolean isFoggy, Runnable setupFog) {
|
||||||
|
AgeSkyProfile sky = ClientAgeCelestialAngles.sky(level.dimension());
|
||||||
|
if (sky.style() != AgeSkyProfile.Style.STATIC && sky.color() == -1 && sky.bodies().isEmpty()
|
||||||
|
&& sky.style() != AgeSkyProfile.Style.NETHER && sky.style() != AgeSkyProfile.Style.END)
|
||||||
|
return false;
|
||||||
|
setupFog.run();
|
||||||
|
if (isFoggy) return true;
|
||||||
|
|
||||||
|
PoseStack pose = new PoseStack();
|
||||||
|
pose.mulPose(modelViewMatrix);
|
||||||
|
Matrix4f matrix = pose.last().pose();
|
||||||
|
Random random = new Random((level.getGameTime() + (long) ticks) / 2L);
|
||||||
|
int tint = sky.color();
|
||||||
|
if (tint == -1) tint = switch (sky.style()) {
|
||||||
|
case NETHER -> 0x6B2725;
|
||||||
|
case END -> 0x211835;
|
||||||
|
case STATIC -> 0x929292;
|
||||||
|
default -> level.getBiome(camera.getBlockPosition()).value().getSkyColor();
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderSystem.depthMask(false);
|
||||||
|
RenderSystem.disableCull();
|
||||||
|
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
||||||
|
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||||
|
FogRenderer.setupNoFog();
|
||||||
|
BufferBuilder vertices = Tesselator.getInstance().begin(
|
||||||
|
VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
||||||
|
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++) {
|
||||||
|
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 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);
|
||||||
|
vertex(vertices, matrix, axis, sign, left, bottom, color);
|
||||||
|
vertex(vertices, matrix, axis, sign, right, bottom, color);
|
||||||
|
vertex(vertices, matrix, axis, sign, right, top, color);
|
||||||
|
vertex(vertices, matrix, axis, sign, left, top, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BufferUploader.drawWithShader(vertices.buildOrThrow());
|
||||||
|
if (!sky.bodies().isEmpty()) {
|
||||||
|
RenderSystem.enableBlend();
|
||||||
|
RenderSystem.defaultBlendFunc();
|
||||||
|
double time = level.getGameTime() + partialTick;
|
||||||
|
for (AgeSkyProfile.Body body : sky.bodies()) drawBody(pose, body, time);
|
||||||
|
RenderSystem.disableBlend();
|
||||||
|
}
|
||||||
|
setupFog.run();
|
||||||
|
RenderSystem.enableCull();
|
||||||
|
RenderSystem.depthMask(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawBody(PoseStack pose, AgeSkyProfile.Body body, double time) {
|
||||||
|
pose.pushPose();
|
||||||
|
pose.mulPose(Axis.YP.rotationDegrees(body.angle()));
|
||||||
|
double orbit = body.period() == 0 ? body.phase()
|
||||||
|
: (body.clockwise() ? 1.0D : -1.0D) * time
|
||||||
|
/ (24000.0D * body.period()) + body.phase();
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
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());
|
||||||
|
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void vertex(BufferBuilder vertices, Matrix4f matrix, int axis, int sign,
|
||||||
|
float x, float y, int color) {
|
||||||
|
float fixed = sign * RADIUS;
|
||||||
|
switch (axis) {
|
||||||
|
case 0 -> vertices.addVertex(matrix, fixed, x, y).setColor(color);
|
||||||
|
case 1 -> vertices.addVertex(matrix, x, fixed, y).setColor(color);
|
||||||
|
default -> vertices.addVertex(matrix, x, y, fixed).setColor(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -196,7 +196,7 @@ public final class SymbolPageTextures {
|
|||||||
graphics.setColor(1F, 1F, 1F, 1F);
|
graphics.setColor(1F, 1F, 1F, 1F);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Legacy's single-number marks; numbers above 24 use positional base 25. */
|
/** Overlay each Legacy digit's components; place base-25 digits side by side. */
|
||||||
public static void drawNumeral(GuiGraphics graphics, int number, int x, int y, int size) {
|
public static void drawNumeral(GuiGraphics graphics, int number, int x, int y, int size) {
|
||||||
int[] digits = SymbolNumerals.digitsFor(number);
|
int[] digits = SymbolNumerals.digitsFor(number);
|
||||||
int digitSize = digits.length == 1 ? size : Math.max(8, size * 3 / 4);
|
int digitSize = digits.length == 1 ? size : Math.max(8, size * 3 / 4);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.menu.LinkModifierMenu;
|
|||||||
import net.dnorberg.rivencraft.network.SetLinkModifierTitlePayload;
|
import net.dnorberg.rivencraft.network.SetLinkModifierTitlePayload;
|
||||||
import net.dnorberg.rivencraft.network.SetLinkModifierSeedPayload;
|
import net.dnorberg.rivencraft.network.SetLinkModifierSeedPayload;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.EditBox;
|
import net.minecraft.client.gui.components.EditBox;
|
||||||
import net.minecraft.client.gui.components.Tooltip;
|
import net.minecraft.client.gui.components.Tooltip;
|
||||||
@@ -137,9 +138,12 @@ public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifi
|
|||||||
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
|
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
|
||||||
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||||
LinkProperties properties = menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
LinkProperties properties = menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||||
for (int index = 0; index < FLAGS.length; index++)
|
for (int index = 0; index < FLAGS.length; index++) {
|
||||||
|
boolean selected = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
|
||||||
|
? platformEnabled(properties) : properties.has(FLAGS[index]);
|
||||||
square(graphics, 5 + (index / 3) * 20, 10 + (index % 3) * 20,
|
square(graphics, 5 + (index / 3) * 20, 10 + (index % 3) * 20,
|
||||||
properties.has(FLAGS[index]), false);
|
selected, false);
|
||||||
|
}
|
||||||
if (menu.canDelete(minecraft.player)) {
|
if (menu.canDelete(minecraft.player)) {
|
||||||
square(graphics, 120, 32, menu.awaitingDeletionConfirmation(), false);
|
square(graphics, 120, 32, menu.awaitingDeletionConfirmation(), false);
|
||||||
if (menu.awaitingDeletionConfirmation()) square(graphics, 140, 32, false, true);
|
if (menu.awaitingDeletionConfirmation()) square(graphics, 140, 32, false, true);
|
||||||
@@ -166,8 +170,8 @@ public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifi
|
|||||||
if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) {
|
if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) {
|
||||||
Component label = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
|
Component label = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
|
||||||
? Component.literal("Generate Platform: "
|
? Component.literal("Generate Platform: "
|
||||||
+ (menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
|
+ (platformEnabled(menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(),
|
||||||
.has(LinkProperties.DISABLE_PLATFORM) ? "Off" : "On"))
|
LinkProperties.NONE)) ? "On" : "Off"))
|
||||||
: LinkProperties.name(FLAGS[index]);
|
: LinkProperties.name(FLAGS[index]);
|
||||||
graphics.renderTooltip(font, label, mouseX, mouseY);
|
graphics.renderTooltip(font, label, mouseX, mouseY);
|
||||||
return;
|
return;
|
||||||
@@ -180,4 +184,10 @@ public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifi
|
|||||||
? "screen.rivencraft.link_modifier.confirm" : "screen.rivencraft.link_modifier.retire"),
|
? "screen.rivencraft.link_modifier.confirm" : "screen.rivencraft.link_modifier.retire"),
|
||||||
mouseX, mouseY);
|
mouseX, mouseY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean platformEnabled(LinkProperties properties) {
|
||||||
|
return menu.book().is(ModItems.LINKING_BOOK.get())
|
||||||
|
? properties.has(LinkProperties.GENERATE_PLATFORM)
|
||||||
|
: !properties.has(LinkProperties.DISABLE_PLATFORM);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -151,8 +151,8 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
|||||||
if (tab == menu.activeCollection() && !allMode) graphics.setColor(0.5F, 0.5F, 1F, 1F);
|
if (tab == menu.activeCollection() && !allMode) graphics.setColor(0.5F, 0.5F, 1F, 1F);
|
||||||
graphics.blit(LEGACY_DESK, x, tabY, 0, 166, 58, 37);
|
graphics.blit(LEGACY_DESK, x, tabY, 0, 166, 58, 37);
|
||||||
graphics.setColor(1F, 1F, 1F, 1F);
|
graphics.setColor(1F, 1F, 1F, 1F);
|
||||||
// Encode the visible one-based tab number with Legacy's numeric word glyph.
|
// Legacy numbers its Writing Desk collections from zero.
|
||||||
SymbolPageTextures.drawNumeral(graphics, tab + 1, x + 8, tabY + 3, 19);
|
SymbolPageTextures.drawNumeral(graphics, tab, x + (tab < 25 ? 8 : 4), tabY + 3, 19);
|
||||||
ItemStack collection = menu.getSlot(tab).getItem();
|
ItemStack collection = menu.getSlot(tab).getItem();
|
||||||
if (!collection.isEmpty()) {
|
if (!collection.isEmpty()) {
|
||||||
String name = collection.getHoverName().getString();
|
String name = collection.getHoverName().getString();
|
||||||
@@ -227,7 +227,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
|||||||
else if (mouseX < leftPos + 34 && localY >= 29 && localY < 177) {
|
else if (mouseX < leftPos + 34 && localY >= 29 && localY < 177) {
|
||||||
int tab = menu.firstVisibleTab() + (localY - 29) / 37;
|
int tab = menu.firstVisibleTab() + (localY - 29) / 37;
|
||||||
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.tab_symbol",
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.tab_symbol",
|
||||||
tab + 1), mouseX, mouseY);
|
tab), mouseX, mouseY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ import net.dnorberg.rivencraft.Rivencraft;
|
|||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||||
import net.dnorberg.rivencraft.writing.InkModifierIngredients;
|
import net.dnorberg.rivencraft.writing.InkModifierIngredients;
|
||||||
import net.dnorberg.rivencraft.writing.PageContent;
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.network.chat.MutableComponent;
|
import net.minecraft.network.chat.MutableComponent;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -63,6 +65,11 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
|||||||
case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id();
|
case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
registration.registerSubtypeInterpreter(ModItems.SYMBOL_PORTFOLIO.get(),
|
||||||
|
(IIngredientSubtypeInterpreter<ItemStack>) (stack, context) -> {
|
||||||
|
Component name = stack.get(DataComponents.CUSTOM_NAME);
|
||||||
|
return name == null ? "empty" : name.getString();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -72,6 +79,7 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
|||||||
var connection = Minecraft.getInstance().getConnection();
|
var connection = Minecraft.getInstance().getConnection();
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
|
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
|
||||||
|
pages.addAll(ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess()));
|
||||||
} else {
|
} else {
|
||||||
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
|
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
|
||||||
.forEach(pages::add);
|
.forEach(pages::add);
|
||||||
@@ -84,6 +92,8 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
|||||||
// JEI discovers the base Page item as a blank stack; only authored pages belong in its list.
|
// JEI discovers the base Page item as a blank stack; only authored pages belong in its list.
|
||||||
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
|
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
|
||||||
List.of(new ItemStack(ModItems.PAGE.get())), List.of(UidContext.Ingredient));
|
List.of(new ItemStack(ModItems.PAGE.get())), List.of(UidContext.Ingredient));
|
||||||
|
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
|
||||||
|
List.of(new ItemStack(ModItems.SYMBOL_PORTFOLIO.get())), List.of(UidContext.Ingredient));
|
||||||
var connection = Minecraft.getInstance().getConnection();
|
var connection = Minecraft.getInstance().getConnection();
|
||||||
if (connection == null) return;
|
if (connection == null) return;
|
||||||
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
|
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
|
||||||
@@ -98,6 +108,19 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
|||||||
if (!missing.isEmpty()) {
|
if (!missing.isEmpty()) {
|
||||||
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing);
|
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing);
|
||||||
}
|
}
|
||||||
|
Set<String> knownPortfolios = runtime.getIngredientManager().getAllItemStacks().stream()
|
||||||
|
.filter(stack -> stack.is(ModItems.SYMBOL_PORTFOLIO.get()))
|
||||||
|
.map(stack -> stack.get(DataComponents.CUSTOM_NAME))
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.map(Component::getString)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
List<ItemStack> missingPortfolios = ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess())
|
||||||
|
.stream()
|
||||||
|
.filter(stack -> !knownPortfolios.contains(stack.getHoverName().getString()))
|
||||||
|
.toList();
|
||||||
|
if (!missingPortfolios.isEmpty()) {
|
||||||
|
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missingPortfolios);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package net.dnorberg.rivencraft.event;
|
|||||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeSkyProfile;
|
||||||
import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
|
import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
||||||
import net.neoforged.neoforge.network.PacketDistributor;
|
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. */
|
/** Sends saved celestial angles when a client joins or enters an Age. */
|
||||||
public final class AgeCelestialAngleSync {
|
public final class AgeCelestialAngleSync {
|
||||||
@@ -34,10 +34,8 @@ public final class AgeCelestialAngleSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void send(ServerPlayer player, AgeDefinition age) {
|
public static void send(ServerPlayer player, AgeDefinition age) {
|
||||||
if (!NetworkRegistry.hasChannel(player.connection, AgeCelestialAnglePayload.TYPE.id())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload(
|
PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload(
|
||||||
AgeDimensionService.dimensionKey(age), age.traits().celestialAngle()));
|
AgeDimensionService.dimensionKey(age), age.traits().celestialAngle(),
|
||||||
|
AgeSkyProfile.forAge(age)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@ import net.dnorberg.rivencraft.block.CrystalPortalBlock;
|
|||||||
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
||||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkingService;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
|
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
|
||||||
import net.dnorberg.rivencraft.portal.CrystalPortalSurfaces;
|
import net.dnorberg.rivencraft.portal.CrystalPortalSurfaces;
|
||||||
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
|
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
|
||||||
@@ -19,7 +22,11 @@ import net.minecraft.gametest.framework.GameTestHelper;
|
|||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.phys.BlockHitResult;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
@@ -154,13 +161,54 @@ public final class CrystalPortalGameTests {
|
|||||||
BlockPos holderPos = helper.absolutePos(new BlockPos(0, 2, 1));
|
BlockPos holderPos = helper.absolutePos(new BlockPos(0, 2, 1));
|
||||||
level.setBlock(holderPos, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState()
|
level.setBlock(holderPos, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState()
|
||||||
.setValue(BookReceptacleBlock.FACING, Direction.UP), 3);
|
.setValue(BookReceptacleBlock.FACING, Direction.UP), 3);
|
||||||
((BookReceptacleBlockEntity) level.getBlockEntity(holderPos))
|
LinkTarget destination = new LinkTarget(GlobalPos.of(level.dimension(), centerTarget(helper)), 0, 0);
|
||||||
.setBook(new ItemStack(ModItems.LINKING_BOOK.get()));
|
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
|
||||||
|
book.set(ModDataComponents.LINK_TARGET.get(), destination);
|
||||||
|
BookReceptacleBlockEntity holder = (BookReceptacleBlockEntity) level.getBlockEntity(holderPos);
|
||||||
|
holder.setBook(book);
|
||||||
|
holder.setDestination(destination);
|
||||||
helper.assertTrue(CrystalPortalNetwork.activate(level, holderPos, planLoaded(level, holderPos)),
|
helper.assertTrue(CrystalPortalNetwork.activate(level, holderPos, planLoaded(level, holderPos)),
|
||||||
"The horizontal crystal opening must activate");
|
"The horizontal crystal opening must activate");
|
||||||
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
|
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
|
||||||
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
|
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
|
||||||
"The fallback surface must face up and down");
|
"The fallback surface must face up and down");
|
||||||
|
helper.assertValueEqual(CrystalPortalSurfaces.group(level, planLoaded(level, holderPos)).getFirst().normal(),
|
||||||
|
Direction.UP, "The horizontal opening must provide an upward-facing destination view");
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
helper.assertValueEqual(LinkingService.travelPortal(player, destination, LinkProperties.NONE),
|
||||||
|
LinkingService.LinkResult.INTRA_LINKING_REQUIRED,
|
||||||
|
"A normal book may show a portal but must not link within its own dimension");
|
||||||
|
helper.assertTrue(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
|
||||||
|
.is(ModBlocks.CRYSTAL_PORTAL.get()) && !holder.getBook().isEmpty(),
|
||||||
|
"Rejected intra-link travel must leave the visual portal and its book intact");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void undersideReceptaclePlacesAndActivatesHorizontalFrame(GameTestHelper helper) {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) {
|
||||||
|
level.setBlock(helper.absolutePos(new BlockPos(x, 2, z)),
|
||||||
|
x == 1 && z == 1 ? Blocks.AIR.defaultBlockState()
|
||||||
|
: ModBlocks.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||||
|
}
|
||||||
|
BlockPos support = helper.absolutePos(new BlockPos(0, 2, 1));
|
||||||
|
BlockPos holderPos = support.below();
|
||||||
|
BlockPlaceContext context = new BlockPlaceContext(level, null, InteractionHand.MAIN_HAND,
|
||||||
|
new ItemStack(ModItems.BOOK_RECEPTACLE.get()),
|
||||||
|
new BlockHitResult(Vec3.atCenterOf(support).add(0, -0.5, 0),
|
||||||
|
Direction.DOWN, support, false));
|
||||||
|
var placed = ModBlocks.BOOK_RECEPTACLE.get().getStateForPlacement(context);
|
||||||
|
helper.assertTrue(placed != null && placed.getValue(BookReceptacleBlock.FACING) == Direction.DOWN,
|
||||||
|
"Clicking the underside of a Crystal Block must place a downward-facing receptacle");
|
||||||
|
level.setBlock(holderPos, placed, 3);
|
||||||
|
((BookReceptacleBlockEntity) level.getBlockEntity(holderPos))
|
||||||
|
.setBook(new ItemStack(ModItems.LINKING_BOOK.get()));
|
||||||
|
helper.assertTrue(CrystalPortalNetwork.activate(level, holderPos, planLoaded(level, holderPos)),
|
||||||
|
"A ceiling-mounted receptacle must activate its horizontal crystal frame");
|
||||||
|
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 2, 1)))
|
||||||
|
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
|
||||||
|
"The underside-mounted portal surface remains horizontal");
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
|
@GameTestHolder(Rivencraft.MOD_ID)
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class SymbolPortfolioGameTests {
|
||||||
|
private SymbolPortfolioGameTests() {}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void portfoliosCoverEverySymbolCategoryWithoutTruncation(GameTestHelper helper) {
|
||||||
|
var registries = helper.getLevel().registryAccess();
|
||||||
|
List<ItemStack> creative = ModCreativeTabs.creativeSymbolPortfolios(registries);
|
||||||
|
helper.assertTrue(!creative.isEmpty(), "Creative tab must include prefilled portfolios");
|
||||||
|
for (ResourceLocation category : PageItem.bundledCreativeCategories()) {
|
||||||
|
List<ItemStack> expected = PageItem.creativeSymbolPages(registries, category);
|
||||||
|
List<ItemStack> volumes = SymbolPortfolioItem.createVolumes(category, expected);
|
||||||
|
helper.assertTrue(!volumes.isEmpty(), "Missing portfolio for " + category);
|
||||||
|
List<ItemStack> actual = new ArrayList<>();
|
||||||
|
for (ItemStack volume : volumes) {
|
||||||
|
helper.assertTrue(volume.is(ModItems.SYMBOL_PORTFOLIO.get())
|
||||||
|
&& CollationFolderItem.isCollection(volume)
|
||||||
|
&& creative.stream().anyMatch(candidate ->
|
||||||
|
candidate.getHoverName().getString().equals(volume.getHoverName().getString())),
|
||||||
|
"Portfolio must be an editable creative collection for " + category);
|
||||||
|
NotebookContents contents = volume.get(ModDataComponents.NOTEBOOK_CONTENTS.get());
|
||||||
|
helper.assertTrue(contents != null && contents.pageCount() > 0
|
||||||
|
&& contents.pageCount() <= NotebookContents.MAX_PAGES,
|
||||||
|
"Each portfolio volume must contain valid pages");
|
||||||
|
helper.assertTrue(contents.pages().stream().allMatch(page ->
|
||||||
|
PageItem.content(page).kind() == PageContent.Kind.SYMBOL),
|
||||||
|
"Portfolios must contain symbol pages, not Link Panels");
|
||||||
|
actual.addAll(contents.pages());
|
||||||
|
}
|
||||||
|
helper.assertTrue(actual.size() == expected.size(), "Portfolio omitted pages in " + category);
|
||||||
|
for (int index = 0; index < expected.size(); index++) {
|
||||||
|
helper.assertTrue(ItemStack.isSameItemSameComponents(actual.get(index), expected.get(index)),
|
||||||
|
"Portfolio pages must match the category tab in " + category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,11 +151,23 @@ public final class WritingDataGameTests {
|
|||||||
"Zero must use Legacy's dedicated number mark");
|
"Zero must use Legacy's dedicated number mark");
|
||||||
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(1), new int[] {56}),
|
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(1), new int[] {56}),
|
||||||
"One must use Legacy's first bottom-row mark");
|
"One must use Legacy's first bottom-row mark");
|
||||||
|
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(5), new int[] {60}),
|
||||||
|
"Five must use Legacy's first five-step mark");
|
||||||
|
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(10), new int[] {61}),
|
||||||
|
"Ten must use Legacy's second five-step mark");
|
||||||
|
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(14), new int[] {61, 59}),
|
||||||
|
"Fourteen must overlay the ten and four marks");
|
||||||
|
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(20), new int[] {63}),
|
||||||
|
"Twenty must use Legacy's final five-step mark");
|
||||||
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(24), new int[] {63, 59}),
|
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(24), new int[] {63, 59}),
|
||||||
"Twenty-four must combine Legacy's five and unit marks");
|
"Twenty-four must combine Legacy's five and unit marks");
|
||||||
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(25), new int[] {1, 0}),
|
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(25), new int[] {1, 0}),
|
||||||
"Twenty-five must start a second base-25 place");
|
"Twenty-five must start a second base-25 place");
|
||||||
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(88), new int[] {3, 13}),
|
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(49), new int[] {1, 24}),
|
||||||
|
"Forty-nine must use twenty-four in its ones place");
|
||||||
|
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(50), new int[] {2, 0}),
|
||||||
|
"Fifty must carry into the second base-25 place");
|
||||||
|
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(87), new int[] {3, 12}),
|
||||||
"Last writing-desk tab must retain its numeric value");
|
"Last writing-desk tab must retain its numeric value");
|
||||||
|
|
||||||
String display = "\"name\":\"symbol.example.test\","
|
String display = "\"name\":\"symbol.example.test\","
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ public final class CollationFolderItem extends NotebookItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isCollection(ItemStack stack) {
|
public static boolean isCollection(ItemStack stack) {
|
||||||
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get());
|
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get())
|
||||||
|
|| stack.is(ModItems.SYMBOL_PORTFOLIO.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int capacity(ItemStack stack) {
|
public static int capacity(ItemStack stack) {
|
||||||
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES
|
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES
|
||||||
: stack.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES : 0;
|
: stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.SYMBOL_PORTFOLIO.get())
|
||||||
|
? NotebookContents.MAX_PAGES : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,7 @@ public class NotebookItem extends Item {
|
|||||||
NotebookContents contents = stack.getOrDefault(
|
NotebookContents contents = stack.getOrDefault(
|
||||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
|
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
|
||||||
tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
|
tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
|
||||||
contents.pageCount(), stack.is(net.dnorberg.rivencraft.registry.ModItems.COLLATION_FOLDER.get())
|
contents.pageCount(), CollationFolderItem.capacity(stack))
|
||||||
? CollationFolderItem.MAX_PAGES : NotebookContents.MAX_PAGES)
|
|
||||||
.withStyle(ChatFormatting.GRAY));
|
.withStyle(ChatFormatting.GRAY));
|
||||||
if (contents.unresolvedCount() > 0) {
|
if (contents.unresolvedCount() > 0) {
|
||||||
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
|
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package net.dnorberg.rivencraft.item;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
/** Non-craftable, prefilled Legacy-style collections of one symbol-page category. */
|
||||||
|
public final class SymbolPortfolioItem extends NotebookItem {
|
||||||
|
public SymbolPortfolioItem(Properties properties) {
|
||||||
|
super(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ItemStack> createVolumes(ResourceLocation category, List<ItemStack> pages) {
|
||||||
|
if (pages.isEmpty()) return List.of();
|
||||||
|
int capacity = NotebookContents.MAX_PAGES;
|
||||||
|
int volumeCount = (pages.size() + capacity - 1) / capacity;
|
||||||
|
List<ItemStack> volumes = new ArrayList<>(volumeCount);
|
||||||
|
for (int volume = 0; volume < volumeCount; volume++) {
|
||||||
|
ItemStack portfolio = new ItemStack(ModItems.SYMBOL_PORTFOLIO.get());
|
||||||
|
int first = volume * capacity;
|
||||||
|
int last = Math.min(first + capacity, pages.size());
|
||||||
|
portfolio.set(ModDataComponents.NOTEBOOK_CONTENTS.get(),
|
||||||
|
new NotebookContents(pages.subList(first, last)));
|
||||||
|
Component categoryName = Component.translatableWithFallback(
|
||||||
|
"itemGroup.rivencraft.pages." + category.getPath(), category.getPath());
|
||||||
|
portfolio.set(DataComponents.CUSTOM_NAME, volumeCount == 1
|
||||||
|
? Component.translatable("item.rivencraft.symbol_portfolio.named", categoryName)
|
||||||
|
: Component.translatable("item.rivencraft.symbol_portfolio.volume",
|
||||||
|
categoryName, volume + 1, volumeCount));
|
||||||
|
volumes.add(portfolio);
|
||||||
|
}
|
||||||
|
return List.copyOf(volumes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,7 +142,8 @@ public final class LinkModifierMenu extends AbstractContainerMenu {
|
|||||||
case TOGGLE_INTRA -> LinkProperties.INTRA_LINKING;
|
case TOGGLE_INTRA -> LinkProperties.INTRA_LINKING;
|
||||||
case TOGGLE_INTRA_ONLY -> LinkProperties.INTRA_LINKING_ONLY;
|
case TOGGLE_INTRA_ONLY -> LinkProperties.INTRA_LINKING_ONLY;
|
||||||
case TOGGLE_RELATIVE -> LinkProperties.RELATIVE_LINK;
|
case TOGGLE_RELATIVE -> LinkProperties.RELATIVE_LINK;
|
||||||
case TOGGLE_PLATFORM -> LinkProperties.DISABLE_PLATFORM;
|
case TOGGLE_PLATFORM -> book().is(ModItems.LINKING_BOOK.get())
|
||||||
|
? LinkProperties.GENERATE_PLATFORM : LinkProperties.DISABLE_PLATFORM;
|
||||||
default -> 0;
|
default -> 0;
|
||||||
};
|
};
|
||||||
if (flag == 0 || ((flag == LinkProperties.INTRA_LINKING || flag == LinkProperties.INTRA_LINKING_ONLY)
|
if (flag == 0 || ((flag == LinkProperties.INTRA_LINKING || flag == LinkProperties.INTRA_LINKING_ONLY)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeSkyProfile;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
/** Combines multiple celestial light sources into Minecraft's one skylight channel. */
|
||||||
|
@Mixin(Level.class)
|
||||||
|
public abstract class LevelSkyBrightnessMixin {
|
||||||
|
@Shadow private int skyDarken;
|
||||||
|
|
||||||
|
@Inject(method = "updateSkyBrightness", at = @At("TAIL"))
|
||||||
|
private void rivencraft$stackCelestialLight(CallbackInfo callback) {
|
||||||
|
if (!((Object) this instanceof ServerLevel level) || !level.dimensionType().hasSkyLight()) return;
|
||||||
|
// ServerLevel updates sky brightness in its constructor, before the
|
||||||
|
// MinecraftServer has registered even the Overworld level. Only an
|
||||||
|
// established Rivencraft Age can have a saved celestial profile.
|
||||||
|
if (!level.dimension().location().getNamespace().equals(Rivencraft.MOD_ID)
|
||||||
|
|| !level.dimension().location().getPath().startsWith("age/")
|
||||||
|
|| level.getServer().overworld() == null) return;
|
||||||
|
AgeRegistrySavedData.get(level).definitions().stream()
|
||||||
|
.filter(age -> AgeDimensionService.dimensionKey(age).equals(level.dimension()))
|
||||||
|
.findFirst().ifPresent(age -> {
|
||||||
|
AgeSkyProfile sky = AgeSkyProfile.forAge(age);
|
||||||
|
if (sky.bodies().isEmpty()) return;
|
||||||
|
float light = sky.lightAt(level.getDayTime());
|
||||||
|
float weather = (1.0F - level.getRainLevel(1.0F) * 5.0F / 16.0F)
|
||||||
|
* (1.0F - level.getThunderLevel(1.0F) * 5.0F / 16.0F);
|
||||||
|
skyDarken = (int) ((1.0F - light * weather) * 11.0F);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import net.minecraft.world.entity.EquipmentSlot;
|
||||||
|
import net.minecraft.world.entity.Mob;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||||
|
|
||||||
|
@Mixin(Mob.class)
|
||||||
|
public interface MobDropChanceAccessor {
|
||||||
|
@Invoker("getEquipmentDropChance")
|
||||||
|
float rivencraft$getEquipmentDropChance(EquipmentSlot slot);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin.client;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeSkyProfile;
|
||||||
|
import net.minecraft.client.multiplayer.ClientLevel;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
/** Keeps authored color pages visible in the sky without replacing vanilla sun and stars. */
|
||||||
|
@Mixin(ClientLevel.class)
|
||||||
|
public abstract class ClientLevelSkyColorMixin {
|
||||||
|
@Inject(method = "getSkyColor", at = @At("RETURN"), cancellable = true)
|
||||||
|
private void rivencraft$colorAuthoredSky(Vec3 position, float partialTick,
|
||||||
|
CallbackInfoReturnable<Vec3> result) {
|
||||||
|
ClientLevel level = (ClientLevel) (Object) this;
|
||||||
|
int color = ClientAgeCelestialAngles.skyColor(level.dimension());
|
||||||
|
if (color == -1 || ClientAgeCelestialAngles.staticSky(level.dimension())) return;
|
||||||
|
Vec3 natural = result.getReturnValue();
|
||||||
|
// Retain the vanilla day/night and precipitation darkening while replacing its hue.
|
||||||
|
AgeSkyProfile sky = ClientAgeCelestialAngles.sky(level.dimension());
|
||||||
|
double brightness = sky.bodies().isEmpty()
|
||||||
|
? Math.max(natural.x, Math.max(natural.y, natural.z))
|
||||||
|
: Math.max(0.08D, sky.lightAt(level.getDayTime() + partialTick));
|
||||||
|
result.setReturnValue(Vec3.fromRGB24(color).scale(brightness));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "getSkyDarken", at = @At("RETURN"), cancellable = true)
|
||||||
|
private void rivencraft$stackClientCelestialLight(float partialTick,
|
||||||
|
CallbackInfoReturnable<Float> result) {
|
||||||
|
ClientLevel level = (ClientLevel) (Object) this;
|
||||||
|
AgeSkyProfile sky = ClientAgeCelestialAngles.sky(level.dimension());
|
||||||
|
if (sky.bodies().isEmpty() || !level.dimensionType().hasSkyLight()) return;
|
||||||
|
float weather = (1.0F - level.getRainLevel(partialTick) * 5.0F / 16.0F)
|
||||||
|
* (1.0F - level.getThunderLevel(partialTick) * 5.0F / 16.0F);
|
||||||
|
result.setReturnValue(0.2F + 0.8F * sky.lightAt(level.getDayTime() + partialTick) * weather);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,20 +2,37 @@ package net.dnorberg.rivencraft.mixin.client;
|
|||||||
|
|
||||||
import com.mojang.math.Axis;
|
import com.mojang.math.Axis;
|
||||||
import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
|
import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
|
||||||
|
import net.dnorberg.rivencraft.client.RivencraftSkyEffects;
|
||||||
|
import net.minecraft.client.Camera;
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
import net.minecraft.client.multiplayer.ClientLevel;
|
||||||
import net.minecraft.client.renderer.LevelRenderer;
|
import net.minecraft.client.renderer.LevelRenderer;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
import org.joml.Quaternionf;
|
import org.joml.Quaternionf;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.Shadow;
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
/** Rotates the vanilla sun, moon, and star path by each Age's saved angle. */
|
/** Rotates the vanilla sun, moon, and star path by each Age's saved angle. */
|
||||||
@Mixin(LevelRenderer.class)
|
@Mixin(LevelRenderer.class)
|
||||||
public abstract class LevelRendererMixin {
|
public abstract class LevelRendererMixin {
|
||||||
|
private static final RivencraftSkyEffects RIVENCRAFT_SKY = new RivencraftSkyEffects();
|
||||||
|
|
||||||
@Shadow
|
@Shadow
|
||||||
private ClientLevel level;
|
private ClientLevel level;
|
||||||
|
|
||||||
|
@Inject(method = "renderSky", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void rivencraft$renderAgeSky(Matrix4f frustumMatrix, Matrix4f projectionMatrix,
|
||||||
|
float partialTick, Camera camera, boolean isFoggy,
|
||||||
|
Runnable skyFogSetup, CallbackInfo callback) {
|
||||||
|
if (!level.dimension().location().getNamespace().equals("rivencraft")) return;
|
||||||
|
ClientAgeCelestialAngles.requestIfMissing(level.dimension());
|
||||||
|
if (RIVENCRAFT_SKY.renderSky(level, (int) level.getGameTime(), partialTick,
|
||||||
|
frustumMatrix, camera, projectionMatrix, isFoggy, skyFogSetup)) callback.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
@Redirect(
|
@Redirect(
|
||||||
method = "renderSky",
|
method = "renderSky",
|
||||||
at = @At(
|
at = @At(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.network;
|
package net.dnorberg.rivencraft.network;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeSkyProfile;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.network.codec.StreamCodec;
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
@@ -9,8 +10,8 @@ import net.minecraft.resources.ResourceKey;
|
|||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
|
|
||||||
/** Synchronizes an Age's saved celestial-plane rotation to clients. */
|
/** Synchronizes an Age's saved celestial-plane rotation and authored sky to clients. */
|
||||||
public record AgeCelestialAnglePayload(ResourceKey<Level> dimension, float angle)
|
public record AgeCelestialAnglePayload(ResourceKey<Level> dimension, float angle, AgeSkyProfile sky)
|
||||||
implements CustomPacketPayload {
|
implements CustomPacketPayload {
|
||||||
public static final Type<AgeCelestialAnglePayload> TYPE = new Type<>(
|
public static final Type<AgeCelestialAnglePayload> TYPE = new Type<>(
|
||||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_celestial_angle")
|
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_celestial_angle")
|
||||||
@@ -20,9 +21,34 @@ public record AgeCelestialAnglePayload(ResourceKey<Level> dimension, float angle
|
|||||||
(buffer, payload) -> {
|
(buffer, payload) -> {
|
||||||
buffer.writeResourceKey(payload.dimension());
|
buffer.writeResourceKey(payload.dimension());
|
||||||
buffer.writeFloat(payload.angle());
|
buffer.writeFloat(payload.angle());
|
||||||
|
buffer.writeEnum(payload.sky().style());
|
||||||
|
buffer.writeInt(payload.sky().color());
|
||||||
|
buffer.writeVarInt(payload.sky().bodies().size());
|
||||||
|
for (AgeSkyProfile.Body body : payload.sky().bodies()) {
|
||||||
|
buffer.writeEnum(body.kind());
|
||||||
|
buffer.writeInt(body.color());
|
||||||
|
buffer.writeFloat(body.size());
|
||||||
|
buffer.writeFloat(body.angle());
|
||||||
|
buffer.writeDouble(body.period());
|
||||||
|
buffer.writeFloat(body.phase());
|
||||||
|
buffer.writeBoolean(body.clockwise());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
buffer -> new AgeCelestialAnglePayload(
|
buffer -> {
|
||||||
buffer.readResourceKey(Registries.DIMENSION), buffer.readFloat())
|
ResourceKey<Level> dimension = buffer.readResourceKey(Registries.DIMENSION);
|
||||||
|
float angle = buffer.readFloat();
|
||||||
|
AgeSkyProfile.Style style = buffer.readEnum(AgeSkyProfile.Style.class);
|
||||||
|
int color = buffer.readInt();
|
||||||
|
int count = buffer.readVarInt();
|
||||||
|
if (count < 0 || count > 20) throw new IllegalArgumentException("Invalid celestial body count");
|
||||||
|
java.util.List<AgeSkyProfile.Body> bodies = new java.util.ArrayList<>(count);
|
||||||
|
for (int i = 0; i < count; i++) bodies.add(new AgeSkyProfile.Body(
|
||||||
|
buffer.readEnum(AgeSkyProfile.Kind.class), buffer.readInt(),
|
||||||
|
buffer.readFloat(), buffer.readFloat(), buffer.readDouble(), buffer.readFloat(),
|
||||||
|
buffer.readBoolean()));
|
||||||
|
return new AgeCelestialAnglePayload(dimension, angle,
|
||||||
|
new AgeSkyProfile(style, color, bodies));
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public final class ModNetworking {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void register(RegisterPayloadHandlersEvent event) {
|
public static void register(RegisterPayloadHandlersEvent event) {
|
||||||
PayloadRegistrar registrar = event.registrar("17");
|
PayloadRegistrar registrar = event.registrar("18");
|
||||||
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
|
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
|
||||||
ModNetworking::requestDescriptiveBookScreen);
|
ModNetworking::requestDescriptiveBookScreen);
|
||||||
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
|
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
|
||||||
@@ -81,6 +81,8 @@ public final class ModNetworking {
|
|||||||
ClientLinkPreviewHandler::handle);
|
ClientLinkPreviewHandler::handle);
|
||||||
registrar.playToClient(AgeCelestialAnglePayload.TYPE, AgeCelestialAnglePayload.STREAM_CODEC,
|
registrar.playToClient(AgeCelestialAnglePayload.TYPE, AgeCelestialAnglePayload.STREAM_CODEC,
|
||||||
ClientAgeCelestialAngles::handle);
|
ClientAgeCelestialAngles::handle);
|
||||||
|
registrar.playToServer(RequestAgeSkyPayload.TYPE, RequestAgeSkyPayload.STREAM_CODEC,
|
||||||
|
ModNetworking::requestAgeSky);
|
||||||
registrar.playToClient(SereneAgeWhitelistPayload.TYPE, SereneAgeWhitelistPayload.STREAM_CODEC,
|
registrar.playToClient(SereneAgeWhitelistPayload.TYPE, SereneAgeWhitelistPayload.STREAM_CODEC,
|
||||||
ClientSereneAgeWhitelist::handle);
|
ClientSereneAgeWhitelist::handle);
|
||||||
registrar.playToClient(WritingDeskCatalogPayload.TYPE, WritingDeskCatalogPayload.STREAM_CODEC,
|
registrar.playToClient(WritingDeskCatalogPayload.TYPE, WritingDeskCatalogPayload.STREAM_CODEC,
|
||||||
@@ -93,6 +95,16 @@ public final class ModNetworking {
|
|||||||
ModNetworking::setLinkModifierSeed);
|
ModNetworking::setLinkModifierSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requestAgeSky(RequestAgeSkyPayload payload, IPayloadContext context) {
|
||||||
|
if (!(context.player() instanceof ServerPlayer player)) return;
|
||||||
|
// Portal renderers may request another Age. Only return registered Age data,
|
||||||
|
// never an arbitrary client-supplied dimension or generated terrain.
|
||||||
|
AgeRegistrySavedData.get(player.serverLevel()).definitions().stream()
|
||||||
|
.filter(age -> AgeDimensionService.dimensionKey(age).equals(payload.dimension()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(age -> AgeCelestialAngleSync.send(player, age));
|
||||||
|
}
|
||||||
|
|
||||||
private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) {
|
private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) {
|
||||||
if (context.player() instanceof ServerPlayer player
|
if (context.player() instanceof ServerPlayer player
|
||||||
&& player.containerMenu instanceof BookBinderMenu menu
|
&& player.containerMenu instanceof BookBinderMenu menu
|
||||||
@@ -348,8 +360,7 @@ public final class ModNetworking {
|
|||||||
|
|
||||||
/** Sky effects, not dimension IDs, decide the backdrop for custom Ages. */
|
/** Sky effects, not dimension IDs, decide the backdrop for custom Ages. */
|
||||||
private static int previewSkyColor(ServerLevel level, BlockPos origin) {
|
private static int previewSkyColor(ServerLevel level, BlockPos origin) {
|
||||||
return AgeEnvironmentCompiler.previewSkyColor(level.dimensionType().effectsLocation(),
|
return AgeEnvironmentCompiler.previewSkyColor(level, origin);
|
||||||
level.getBiome(origin).value().getSkyColor());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InteractionHand hand(boolean offhand) {
|
private static InteractionHand hand(boolean offhand) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net.dnorberg.rivencraft.network;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
|
||||||
|
/** Requests the saved sky profile when a renderer reaches an Age before dimension-change sync. */
|
||||||
|
public record RequestAgeSkyPayload(ResourceKey<Level> dimension) implements CustomPacketPayload {
|
||||||
|
public static final Type<RequestAgeSkyPayload> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "request_age_sky"));
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, RequestAgeSkyPayload> STREAM_CODEC = StreamCodec.of(
|
||||||
|
(buffer, payload) -> buffer.writeResourceKey(payload.dimension()),
|
||||||
|
buffer -> new RequestAgeSkyPayload(buffer.readResourceKey(Registries.DIMENSION)));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ package net.dnorberg.rivencraft.portal;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
/** Session-only portal cooldowns: tick counters must never be saved with players. */
|
/** Session-only portal cooldowns: tick counters must never be saved with players. */
|
||||||
public final class PortalTravelCooldown {
|
public final class PortalTravelCooldown {
|
||||||
public static final long TICKS = 40;
|
public static final long TICKS = 40;
|
||||||
private static final Map<ServerPlayer, Long> LAST_USE = new WeakHashMap<>();
|
private static final Map<Entity, Long> LAST_USE = new WeakHashMap<>();
|
||||||
|
|
||||||
private PortalTravelCooldown() {}
|
private PortalTravelCooldown() {}
|
||||||
|
|
||||||
@@ -15,16 +16,16 @@ public final class PortalTravelCooldown {
|
|||||||
return previous >= 0 && now >= previous && now - previous < TICKS;
|
return previous >= 0 && now >= previous && now - previous < TICKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean blocked(ServerPlayer player, long now) {
|
public static boolean blocked(Entity entity, long now) {
|
||||||
return blocked(now, LAST_USE.getOrDefault(player, -1L));
|
return blocked(now, LAST_USE.getOrDefault(entity, -1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void mark(ServerPlayer player, long now) {
|
public static void mark(Entity entity, long now) {
|
||||||
LAST_USE.put(player, now);
|
LAST_USE.put(entity, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void forget(ServerPlayer player) {
|
public static void forget(Entity entity) {
|
||||||
LAST_USE.remove(player);
|
LAST_USE.remove(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void clear() {
|
public static void clear() {
|
||||||
|
|||||||
@@ -7,34 +7,43 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeReference;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkDisarmService;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkingService;
|
||||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
/** Defers block-collision travel until the movement packet that entered the portal has finished. */
|
/** Defers block-collision travel until the movement packet that entered the portal has finished. */
|
||||||
public final class PortalTravelQueue {
|
public final class PortalTravelQueue {
|
||||||
private static final Map<ServerPlayer, Pending> PENDING = new IdentityHashMap<>();
|
private static final Map<Entity, Pending> PENDING = new IdentityHashMap<>();
|
||||||
|
|
||||||
private record Pending(ServerLevel source, BlockPos portal, BlockPos receptacle,
|
private record Pending(ServerLevel source, BlockPos portal, BlockPos receptacle,
|
||||||
ItemStack book, long queuedTick) {}
|
ItemStack book, long queuedTick) {}
|
||||||
private record Travel(ServerPlayer player, Pending pending) {}
|
private record Travel(Entity entity, Pending pending) {}
|
||||||
|
|
||||||
private PortalTravelQueue() {}
|
private PortalTravelQueue() {}
|
||||||
|
|
||||||
public static void enqueue(ServerPlayer player, ServerLevel source, BlockPos portal,
|
public static void enqueue(Entity entity, ServerLevel source, BlockPos portal,
|
||||||
BlockPos receptacle, ItemStack book, long now) {
|
BlockPos receptacle, ItemStack book, long now) {
|
||||||
PENDING.putIfAbsent(player, new Pending(source, portal.immutable(), receptacle.immutable(),
|
PENDING.putIfAbsent(entity, new Pending(source, portal.immutable(), receptacle.immutable(),
|
||||||
book.copyWithCount(1), now));
|
book.copyWithCount(1), now));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void process(MinecraftServer server) {
|
public static void process(MinecraftServer server) {
|
||||||
List<Travel> ready = new ArrayList<>();
|
List<Travel> ready = new ArrayList<>();
|
||||||
Iterator<Map.Entry<ServerPlayer, Pending>> iterator = PENDING.entrySet().iterator();
|
Iterator<Map.Entry<Entity, Pending>> iterator = PENDING.entrySet().iterator();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
var entry = iterator.next();
|
var entry = iterator.next();
|
||||||
if (server.getTickCount() <= entry.getValue().queuedTick()) continue;
|
if (server.getTickCount() <= entry.getValue().queuedTick()) continue;
|
||||||
@@ -44,21 +53,45 @@ public final class PortalTravelQueue {
|
|||||||
// Travel can itself cause block-collision callbacks, so do not keep an
|
// Travel can itself cause block-collision callbacks, so do not keep an
|
||||||
// iterator over the pending map while changing the player's dimension.
|
// iterator over the pending map while changing the player's dimension.
|
||||||
for (Travel travel : ready) {
|
for (Travel travel : ready) {
|
||||||
ServerPlayer player = travel.player();
|
Entity entity = travel.entity();
|
||||||
Pending pending = travel.pending();
|
Pending pending = travel.pending();
|
||||||
if (server.getPlayerList().getPlayer(player.getUUID()) != player
|
if ((entity instanceof ServerPlayer player
|
||||||
|| !player.isAlive() || player.serverLevel() != pending.source()
|
&& server.getPlayerList().getPlayer(player.getUUID()) != player)
|
||||||
|
|| entity.isRemoved() || !entity.isAlive() || entity.level() != pending.source()
|
||||||
|| !(pending.source().getBlockEntity(pending.portal()) instanceof CrystalPortalBlockEntity portal)
|
|| !(pending.source().getBlockEntity(pending.portal()) instanceof CrystalPortalBlockEntity portal)
|
||||||
|| !pending.receptacle().equals(portal.receptacle())
|
|| !pending.receptacle().equals(portal.receptacle())
|
||||||
|| !(pending.source().getBlockEntity(pending.receptacle()) instanceof BookReceptacleBlockEntity holder)
|
|| !(pending.source().getBlockEntity(pending.receptacle()) instanceof BookReceptacleBlockEntity holder)
|
||||||
|| !ItemStack.isSameItemSameComponents(holder.getBook(), pending.book())) continue;
|
|| !ItemStack.isSameItemSameComponents(holder.getBook(), pending.book())) continue;
|
||||||
if (pending.book().is(ModItems.LINKING_BOOK.get()))
|
if (entity instanceof ServerPlayer player) {
|
||||||
LinkingBookItem.usePortalBook(player, pending.book());
|
if (pending.book().is(ModItems.LINKING_BOOK.get()))
|
||||||
else if (pending.book().is(ModItems.DESCRIPTIVE_BOOK.get()))
|
LinkingBookItem.usePortalBook(player, pending.book());
|
||||||
DescriptiveBookItem.usePortalBook(player, pending.book());
|
else if (pending.book().is(ModItems.DESCRIPTIVE_BOOK.get()))
|
||||||
|
DescriptiveBookItem.usePortalBook(player, pending.book());
|
||||||
|
} else {
|
||||||
|
travelEntity(entity, pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void travelEntity(Entity entity, Pending pending) {
|
||||||
|
ItemStack book = pending.book();
|
||||||
|
LinkProperties properties = book.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||||
|
if (properties.has(LinkProperties.DISARM) && !LinkDisarmService.mayTravel(entity)) return;
|
||||||
|
LinkTarget target;
|
||||||
|
boolean descriptive = book.is(ModItems.DESCRIPTIVE_BOOK.get());
|
||||||
|
if (descriptive) {
|
||||||
|
AgeReference reference = book.get(ModDataComponents.AGE_REFERENCE.get());
|
||||||
|
if (reference == null) return;
|
||||||
|
properties = properties.forDescriptiveBook();
|
||||||
|
target = AgeDimensionService.resolve(pending.source(), reference, OrderedWriting.EMPTY,
|
||||||
|
!properties.has(LinkProperties.DISABLE_PLATFORM)).orElse(null);
|
||||||
|
} else if (book.is(ModItems.LINKING_BOOK.get())) {
|
||||||
|
target = book.get(ModDataComponents.LINK_TARGET.get());
|
||||||
|
} else return;
|
||||||
|
if (target == null) return;
|
||||||
|
LinkingService.travelEntityPortal(entity, target, properties, descriptive);
|
||||||
|
}
|
||||||
|
|
||||||
public static void forget(ServerPlayer player) { PENDING.remove(player); }
|
public static void forget(ServerPlayer player) { PENDING.remove(player); }
|
||||||
public static void clear() { PENDING.clear(); }
|
public static void clear() { PENDING.clear(); }
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,9 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -28,6 +31,7 @@ public final class ModCreativeTabs {
|
|||||||
output.accept(ModItems.LINKING_BOOK.get());
|
output.accept(ModItems.LINKING_BOOK.get());
|
||||||
output.accept(ModItems.NOTEBOOK.get());
|
output.accept(ModItems.NOTEBOOK.get());
|
||||||
output.accept(ModItems.COLLATION_FOLDER.get());
|
output.accept(ModItems.COLLATION_FOLDER.get());
|
||||||
|
creativeSymbolPortfolios(parameters.holders()).forEach(output::accept);
|
||||||
output.accept(ModItems.SEALED_NOTEBOOK.get());
|
output.accept(ModItems.SEALED_NOTEBOOK.get());
|
||||||
output.accept(ModItems.INK_VIAL.get());
|
output.accept(ModItems.INK_VIAL.get());
|
||||||
output.accept(ModItems.WRITING_DESK.get());
|
output.accept(ModItems.WRITING_DESK.get());
|
||||||
@@ -59,14 +63,28 @@ public final class ModCreativeTabs {
|
|||||||
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
||||||
registerSymbolPageTabs();
|
registerSymbolPageTabs();
|
||||||
|
|
||||||
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
|
/** Prefilled portfolios mirror the symbol-page tabs, including data-pack and modded pages. */
|
||||||
|
public static List<ItemStack> creativeSymbolPortfolios(HolderLookup.Provider registries) {
|
||||||
|
List<ItemStack> portfolios = new ArrayList<>();
|
||||||
|
for (ResourceLocation category : orderedSymbolCategories()) {
|
||||||
|
portfolios.addAll(SymbolPortfolioItem.createVolumes(category,
|
||||||
|
PageItem.creativeSymbolPages(registries, category)));
|
||||||
|
}
|
||||||
|
return List.copyOf(portfolios);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ResourceLocation> orderedSymbolCategories() {
|
||||||
List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories());
|
List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories());
|
||||||
categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
|
categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
|
||||||
int index = CATEGORY_ORDER.indexOf(id.getPath());
|
int index = CATEGORY_ORDER.indexOf(id.getPath());
|
||||||
return index < 0 ? CATEGORY_ORDER.size() : index;
|
return index < 0 ? CATEGORY_ORDER.size() : index;
|
||||||
}).thenComparing(ResourceLocation::toString));
|
}).thenComparing(ResourceLocation::toString));
|
||||||
|
return List.copyOf(categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
|
||||||
Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>();
|
Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>();
|
||||||
for (ResourceLocation category : categories) {
|
for (ResourceLocation category : orderedSymbolCategories()) {
|
||||||
String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_');
|
String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_');
|
||||||
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
|
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
|
||||||
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(),
|
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.item.LinkingBookItem;
|
|||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.dnorberg.rivencraft.item.NotebookItem;
|
import net.dnorberg.rivencraft.item.NotebookItem;
|
||||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||||
|
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
|
||||||
import net.dnorberg.rivencraft.item.SealedNotebookItem;
|
import net.dnorberg.rivencraft.item.SealedNotebookItem;
|
||||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
import net.minecraft.world.item.Item;
|
import net.minecraft.world.item.Item;
|
||||||
@@ -39,6 +40,8 @@ public final class ModItems {
|
|||||||
);
|
);
|
||||||
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
|
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
|
||||||
"collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1));
|
"collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1));
|
||||||
|
public static final DeferredItem<SymbolPortfolioItem> SYMBOL_PORTFOLIO = ITEMS.registerItem(
|
||||||
|
"symbol_portfolio", SymbolPortfolioItem::new, new Item.Properties().stacksTo(1));
|
||||||
public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem(
|
public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem(
|
||||||
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
|
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
|
||||||
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package net.dnorberg.rivencraft.writing;
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number components from Mystcraft Legacy's ModWords#constructNumber.
|
* Positional base-25 numerals using Mystcraft Legacy's 0–24 digit components.
|
||||||
* Legacy registers 0–25; larger Rivencraft tab numbers use positional base 25.
|
* Components within one digit are layered in the same square, not laid side by side.
|
||||||
*/
|
*/
|
||||||
public final class SymbolNumerals {
|
public final class SymbolNumerals {
|
||||||
private SymbolNumerals() {}
|
private SymbolNumerals() {}
|
||||||
@@ -19,7 +19,7 @@ public final class SymbolNumerals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int[] componentsForDigit(int digit) {
|
public static int[] componentsForDigit(int digit) {
|
||||||
if (digit < 0 || digit >= 25) throw new IllegalArgumentException("Numeral digit must be in [0, 24]");
|
if (digit < 0 || digit >= 25) throw new IllegalArgumentException("Base-25 digit must be in [0, 24]");
|
||||||
if (digit == 0) return new int[] {1};
|
if (digit == 0) return new int[] {1};
|
||||||
int fifths = digit / 5;
|
int fifths = digit / 5;
|
||||||
int remainder = digit % 5;
|
int remainder = digit % 5;
|
||||||
|
|||||||
@@ -77,12 +77,18 @@ public record SymbolSemantics(Role role, Value value) {
|
|||||||
"native", "single", "tiled", "grid", "tiny", "small", "medium", "large", "huge"),
|
"native", "single", "tiled", "grid", "tiny", "small", "medium", "large", "huge"),
|
||||||
BIOME("biome", Kind.RESOURCE),
|
BIOME("biome", Kind.RESOURCE),
|
||||||
LIGHTING("lighting", Kind.CHOICE, "normal", "dark", "bright"),
|
LIGHTING("lighting", Kind.CHOICE, "normal", "dark", "bright"),
|
||||||
SKY("sky", Kind.CHOICE, "overworld", "nether", "end"),
|
SKY("sky", Kind.CHOICE, "overworld", "nether", "end", "static"),
|
||||||
|
SKY_COLOR("sky_color", Kind.CHOICE,
|
||||||
|
"red", "orange", "yellow", "green", "blue", "purple", "white", "black"),
|
||||||
WEATHER("weather", Kind.CHOICE,
|
WEATHER("weather", Kind.CHOICE,
|
||||||
"normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"),
|
"normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"),
|
||||||
SEASON("season", Kind.CHOICE,
|
SEASON("season", Kind.CHOICE,
|
||||||
"no_seasons", "seasons", "eternal_winter", "eternal_summer", "eternal_fall", "eternal_spring"),
|
"no_seasons", "seasons", "eternal_winter", "eternal_summer", "eternal_fall", "eternal_spring"),
|
||||||
CELESTIAL("celestial", Kind.CHOICE, "normal_time"),
|
CELESTIAL("celestial", Kind.CHOICE, "normal_time", "sun", "moon"),
|
||||||
|
CELESTIAL_COLOR("celestial_color", Kind.CHOICE,
|
||||||
|
"red", "orange", "yellow", "green", "blue", "purple", "white", "black"),
|
||||||
|
DIRECTION_MODIFIER("direction_modifier", Kind.CHOICE, "clockwise", "counterclockwise"),
|
||||||
|
SIZE_MODIFIER("size_modifier", Kind.NUMBER),
|
||||||
PERIOD_MODIFIER("period_modifier", Kind.NUMBER),
|
PERIOD_MODIFIER("period_modifier", Kind.NUMBER),
|
||||||
ANGLE_MODIFIER("angle_modifier", Kind.NUMBER),
|
ANGLE_MODIFIER("angle_modifier", Kind.NUMBER),
|
||||||
PHASE_MODIFIER("phase_modifier", Kind.NUMBER),
|
PHASE_MODIFIER("phase_modifier", Kind.NUMBER),
|
||||||
@@ -109,6 +115,7 @@ public record SymbolSemantics(Role role, Value value) {
|
|||||||
if (!(value instanceof NumberValue number)) return false;
|
if (!(value instanceof NumberValue number)) return false;
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0;
|
case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0;
|
||||||
|
case SIZE_MODIFIER -> number.amount() >= 0.25 && number.amount() <= 3.0;
|
||||||
case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0;
|
case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0;
|
||||||
default -> false;
|
default -> false;
|
||||||
};
|
};
|
||||||
@@ -119,6 +126,7 @@ public record SymbolSemantics(Role role, Value value) {
|
|||||||
case RESOURCE -> "a canonical namespaced ID";
|
case RESOURCE -> "a canonical namespaced ID";
|
||||||
case CHOICE -> "one of " + choices.stream().sorted().toList();
|
case CHOICE -> "one of " + choices.stream().sorted().toList();
|
||||||
case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]"
|
case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]"
|
||||||
|
: this == SIZE_MODIFIER ? "a finite number in [0.25, 3]"
|
||||||
: "a finite number in [0, 360) degrees";
|
: "a finite number in [0, 360) degrees";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ public final class WritingGrammar {
|
|||||||
}
|
}
|
||||||
pendingBlock = null;
|
pendingBlock = null;
|
||||||
}
|
}
|
||||||
if (role == Role.ANGLE_MODIFIER || role == Role.WEATHER
|
if (role == Role.WEATHER
|
||||||
&& parsed.semantics().value() instanceof ChoiceValue choice
|
&& parsed.semantics().value() instanceof ChoiceValue choice
|
||||||
&& UNSUPPORTED_WEATHER.contains(choice.id())) {
|
&& UNSUPPORTED_WEATHER.contains(choice.id())) {
|
||||||
diagnostics.add(new Diagnostic(Code.UNSUPPORTED_ENVIRONMENT, index,
|
diagnostics.add(new Diagnostic(Code.UNSUPPORTED_ENVIRONMENT, index,
|
||||||
@@ -142,7 +142,10 @@ public final class WritingGrammar {
|
|||||||
}
|
}
|
||||||
for (Role required : List.of(Role.TERRAIN, Role.BIOME_CONTROLLER, Role.WEATHER,
|
for (Role required : List.of(Role.TERRAIN, Role.BIOME_CONTROLLER, Role.WEATHER,
|
||||||
Role.LIGHTING, Role.CELESTIAL)) {
|
Role.LIGHTING, Role.CELESTIAL)) {
|
||||||
if (!singular.containsKey(required)) diagnostics.add(new Diagnostic(Code.INCOMPLETE, -1,
|
boolean present = required == Role.CELESTIAL
|
||||||
|
? symbols.stream().anyMatch(symbol -> symbol.semantics().role() == Role.CELESTIAL)
|
||||||
|
: singular.containsKey(required);
|
||||||
|
if (!present) diagnostics.add(new Diagnostic(Code.INCOMPLETE, -1,
|
||||||
"The Age compiler will fill the missing " + required.id() + " branch from the Age seed"));
|
"The Age compiler will fill the missing " + required.id() + " branch from the Age seed"));
|
||||||
}
|
}
|
||||||
if (controller != null && controller.semantics().value() instanceof ChoiceValue choice) {
|
if (controller != null && controller.semantics().value() instanceof ChoiceValue choice) {
|
||||||
@@ -160,13 +163,15 @@ public final class WritingGrammar {
|
|||||||
|
|
||||||
private static boolean singular(Role role) {
|
private static boolean singular(Role role) {
|
||||||
return switch (role) {
|
return switch (role) {
|
||||||
case TERRAIN, BIOME_CONTROLLER, WEATHER, SEASON, LIGHTING, SKY, CELESTIAL -> true;
|
case TERRAIN, BIOME_CONTROLLER, WEATHER, SEASON, LIGHTING, SKY -> true;
|
||||||
default -> false;
|
default -> false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean modifier(Role role) {
|
private static boolean modifier(Role role) {
|
||||||
return role == Role.PERIOD_MODIFIER || role == Role.ANGLE_MODIFIER || role == Role.PHASE_MODIFIER;
|
return role == Role.PERIOD_MODIFIER || role == Role.ANGLE_MODIFIER
|
||||||
|
|| role == Role.PHASE_MODIFIER || role == Role.SIZE_MODIFIER
|
||||||
|
|| role == Role.CELESTIAL_COLOR || role == Role.DIRECTION_MODIFIER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int stage(Role role) {
|
private static int stage(Role role) {
|
||||||
@@ -177,8 +182,9 @@ public final class WritingGrammar {
|
|||||||
case WEATHER -> 3;
|
case WEATHER -> 3;
|
||||||
case SEASON -> 4;
|
case SEASON -> 4;
|
||||||
case LIGHTING -> 5;
|
case LIGHTING -> 5;
|
||||||
case SKY -> 6;
|
case SKY, SKY_COLOR -> 6;
|
||||||
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER -> 7;
|
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER,
|
||||||
|
CELESTIAL_COLOR, DIRECTION_MODIFIER -> 7;
|
||||||
case CELESTIAL -> 8;
|
case CELESTIAL -> 8;
|
||||||
case BLOCK, FEATURE, ENVIRONMENT, CORRUPTED -> 9;
|
case BLOCK, FEATURE, ENVIRONMENT, CORRUPTED -> 9;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"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":270},"facing=down":{"model":"rivencraft:block/book_receptacle","x":90}}}
|
{"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}}}
|
||||||
@@ -18,9 +18,13 @@
|
|||||||
"item.rivencraft.link_property.intra_linking_only": "Intra-Linking Only",
|
"item.rivencraft.link_property.intra_linking_only": "Intra-Linking Only",
|
||||||
"item.rivencraft.link_property.relative_link": "Relative Link",
|
"item.rivencraft.link_property.relative_link": "Relative Link",
|
||||||
"item.rivencraft.link_property.disable_platform": "Generate Platform: Off",
|
"item.rivencraft.link_property.disable_platform": "Generate Platform: Off",
|
||||||
|
"item.rivencraft.link_property.generate_platform": "Generate Platform: On",
|
||||||
"item.rivencraft.page.unresolved": "Unresolved Symbol Page",
|
"item.rivencraft.page.unresolved": "Unresolved Symbol Page",
|
||||||
"item.rivencraft.notebook": "Notebook",
|
"item.rivencraft.notebook": "Notebook",
|
||||||
"item.rivencraft.collation_folder": "Collation Folder",
|
"item.rivencraft.collation_folder": "Collation Folder",
|
||||||
|
"item.rivencraft.symbol_portfolio": "Symbol Portfolio",
|
||||||
|
"item.rivencraft.symbol_portfolio.named": "%s Portfolio",
|
||||||
|
"item.rivencraft.symbol_portfolio.volume": "%s Portfolio %s/%s",
|
||||||
"item.rivencraft.sealed_notebook": "Sealed Notebook",
|
"item.rivencraft.sealed_notebook": "Sealed Notebook",
|
||||||
"block.rivencraft.link_modifier": "Link Modifier",
|
"block.rivencraft.link_modifier": "Link Modifier",
|
||||||
"container.rivencraft.link_modifier": "Link Modifier",
|
"container.rivencraft.link_modifier": "Link Modifier",
|
||||||
@@ -219,6 +223,24 @@
|
|||||||
"symbol.rivencraft.sky.nether.description": "Use the Nether sky and red preview background.",
|
"symbol.rivencraft.sky.nether.description": "Use the Nether sky and red preview background.",
|
||||||
"symbol.rivencraft.sky.end": "End Sky",
|
"symbol.rivencraft.sky.end": "End Sky",
|
||||||
"symbol.rivencraft.sky.end.description": "Use the End sky and dark preview background.",
|
"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.weather.normal": "Normal Weather",
|
"symbol.rivencraft.weather.normal": "Normal Weather",
|
||||||
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
|
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
|
||||||
"symbol.rivencraft.weather.fast": "Fast Weather",
|
"symbol.rivencraft.weather.fast": "Fast Weather",
|
||||||
@@ -245,6 +267,44 @@
|
|||||||
"symbol.rivencraft.season.eternal_spring.description": "Hold this Age in midspring.",
|
"symbol.rivencraft.season.eternal_spring.description": "Hold this Age in midspring.",
|
||||||
"symbol.rivencraft.celestial.normal_time": "Normal Time",
|
"symbol.rivencraft.celestial.normal_time": "Normal Time",
|
||||||
"symbol.rivencraft.celestial.normal_time.description": "Use a normal-length celestial cycle.",
|
"symbol.rivencraft.celestial.normal_time.description": "Use a normal-length celestial cycle.",
|
||||||
|
"symbol.rivencraft.celestial.sun": "Sun",
|
||||||
|
"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",
|
||||||
|
"symbol.rivencraft.size.normal.description": "Set the next sun or moon to normal apparent size.",
|
||||||
|
"symbol.rivencraft.size.large": "Large Celestial",
|
||||||
|
"symbol.rivencraft.size.large.description": "Double the apparent size of the next sun or moon.",
|
||||||
|
"symbol.rivencraft.angle.north": "North Celestial Angle",
|
||||||
|
"symbol.rivencraft.angle.north.description": "Orient the next sun or moon toward the north.",
|
||||||
|
"symbol.rivencraft.angle.east": "East Celestial Angle",
|
||||||
|
"symbol.rivencraft.angle.east.description": "Orient the next sun or moon toward the east.",
|
||||||
|
"symbol.rivencraft.angle.south": "South Celestial Angle",
|
||||||
|
"symbol.rivencraft.angle.south.description": "Orient the next sun or moon toward the south.",
|
||||||
|
"symbol.rivencraft.angle.west": "West Celestial Angle",
|
||||||
|
"symbol.rivencraft.angle.west.description": "Orient the next sun or moon toward the west.",
|
||||||
|
"symbol.rivencraft.direction.clockwise": "Clockwise Motion",
|
||||||
|
"symbol.rivencraft.direction.clockwise.description": "Move the next sun or moon clockwise in its orbital plane.",
|
||||||
|
"symbol.rivencraft.direction.counterclockwise": "Counterclockwise Motion",
|
||||||
|
"symbol.rivencraft.direction.counterclockwise.description": "Move the next sun or moon counterclockwise in its orbital plane.",
|
||||||
"symbol.rivencraft.period.zero": "Zero Length",
|
"symbol.rivencraft.period.zero": "Zero Length",
|
||||||
"symbol.rivencraft.period.zero.description": "Stop the celestial cycle at its written phase.",
|
"symbol.rivencraft.period.zero.description": "Stop the celestial cycle at its written phase.",
|
||||||
"symbol.rivencraft.period.half": "Half Length",
|
"symbol.rivencraft.period.half": "Half Length",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"render_type":"minecraft:translucent","ambientocclusion":false,"textures":{"particle":"rivencraft:block/crystal_portal","portal":"rivencraft:block/crystal_portal"},"elements":[{"from":[0,0,6],"to":[16,16,10],"shade":false,"faces":{"north":{"texture":"#portal"},"south":{"texture":"#portal"}}}]}
|
{"render_type":"minecraft:translucent","ambientocclusion":false,"textures":{"particle":"rivencraft:block/crystal_portal","portal":"rivencraft:block/crystal_portal"},"elements":[{"from":[0,0,6],"to":[16,16,10],"shade":false,"faces":{"north":{"texture":"#portal","tintindex":0},"south":{"texture":"#portal","tintindex":0}}}]}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"parent": "minecraft:item/generated",
|
||||||
|
"textures": { "layer0": "rivencraft:item/symbol_portfolio" }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.angle.east","description":"symbol.rivencraft.angle.east.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":90.0},"poem":"Transform East Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.angle.north","description":"symbol.rivencraft.angle.north.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":0.0},"poem":"Transform North Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.angle.south","description":"symbol.rivencraft.angle.south.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":180.0},"poem":"Transform South Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.angle.west","description":"symbol.rivencraft.angle.west.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":270.0},"poem":"Transform West Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.celestial.moon","description":"symbol.rivencraft.celestial.moon.description","category":"rivencraft:celestial","semantics":{"role":"celestial","value":"moon"},"poem":"Transform Time Shadow Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.celestial.sun","description":"symbol.rivencraft.celestial.sun.description","category":"rivencraft:celestial","semantics":{"role":"celestial","value":"sun"},"poem":"Transform Time Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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.direction.clockwise","description":"symbol.rivencraft.direction.clockwise.description","category":"rivencraft:celestial","semantics":{"role":"direction_modifier","value":"clockwise"},"poem":"Transform Time Clockwise Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.direction.counterclockwise","description":"symbol.rivencraft.direction.counterclockwise.description","category":"rivencraft:celestial","semantics":{"role":"direction_modifier","value":"counterclockwise"},"poem":"Transform Time Reverse Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.size.large","description":"symbol.rivencraft.size.large.description","category":"rivencraft:celestial","semantics":{"role":"size_modifier","value":2.0},"poem":"Transform Large Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.size.normal","description":"symbol.rivencraft.size.normal.description","category":"rivencraft:celestial","semantics":{"role":"size_modifier","value":1.0},"poem":"Transform Balance Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.size.small","description":"symbol.rivencraft.size.small.description","category":"rivencraft:celestial","semantics":{"role":"size_modifier","value":0.5},"poem":"Transform Small Illuminate Motion"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":2,"name":"symbol.rivencraft.sky.static","description":"symbol.rivencraft.sky.static.description","category":"rivencraft:sky","semantics":{"role":"sky","value":"static"},"poem":"Ethereal Void Sky Flux"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
@@ -5,11 +5,14 @@
|
|||||||
"compatibilityLevel": "JAVA_21",
|
"compatibilityLevel": "JAVA_21",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"AgeDimensionSeedMixin",
|
"AgeDimensionSeedMixin",
|
||||||
|
"LevelSkyBrightnessMixin",
|
||||||
"AgeDungeonPlacementMixin",
|
"AgeDungeonPlacementMixin",
|
||||||
"AgeStructureBiomeMixin",
|
"AgeStructureBiomeMixin",
|
||||||
|
"MobDropChanceAccessor",
|
||||||
"StructurePlacementAccessor"
|
"StructurePlacementAccessor"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
|
"client.ClientLevelSkyColorMixin",
|
||||||
"client.ClientLevelSoundMixin",
|
"client.ClientLevelSoundMixin",
|
||||||
"client.ImmersivePortalsGuiRenderingMixin",
|
"client.ImmersivePortalsGuiRenderingMixin",
|
||||||
"client.LevelRendererMixin"
|
"client.LevelRendererMixin"
|
||||||
|
|||||||