Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9044835ec5 | ||
|
|
7ae41da23e | ||
|
|
9cb63e6994 | ||
|
|
6337b5aa10 | ||
|
|
b9faa28dd5 | ||
|
|
09d2b0800c | ||
|
|
ee905cab68 | ||
|
|
5c08f391a2 | ||
|
|
b8d26a9036 | ||
|
|
d0af75239a |
No files matched your search
@@ -118,6 +118,11 @@ 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
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 124 KiB |
@@ -40,6 +40,27 @@ profiles, not claims that Legacy had identically named terrain symbols.
|
||||
Every family retains a safe cobblestone arrival platform. An omitted terrain
|
||||
page still uses the deterministic seed-selected profile.
|
||||
|
||||
### Cave terrain and Legacy
|
||||
|
||||
Legacy's closest terrain generator is its Nether terrain symbol: it owns an
|
||||
Age-seeded octave-noise generator, accepts terrain/sea materials, and sets a
|
||||
sea level of 32. Legacy's separate Caves symbol instead registers a terrain
|
||||
alteration that carves additional caves; it is not a terrain generator.
|
||||
Minecraft 1.21.1 no longer has Legacy's 1.12.2 height and generation pipeline,
|
||||
so Rivencraft Cave terrain uses `minecraft:caves` noise and the modern Cave,
|
||||
Nether, and tagged modded Nether biome decorations. It cannot reproduce the
|
||||
exact Legacy noise or its older vertical layout. The Caves feature page
|
||||
remains a separate additive carver for other suitable terrain.
|
||||
|
||||
Cave Ages search for safe spawn footing from Y=64 downward rather than using
|
||||
the cave roof; if necessary, the usual cloud platform is placed at Y=63.
|
||||
Weather is clear and Serene Seasons is disabled in Cave terrain. Authored
|
||||
Weather, Season, and Celestial pages (including time modifiers) are retained
|
||||
in the book but have no effect on the Cave Age; each adds 100 base instability.
|
||||
The same policy applies to randomly selected Cave terrain and persists through
|
||||
save/reload. Cave biome selections remain limited to Cave and Nether biomes;
|
||||
surface-biome completion is not used for Cave terrain.
|
||||
|
||||
The separate **Floating Islands** feature page adds rare high-altitude island
|
||||
clusters above Standard, Amplified, Sky Islands, Flat, or Void terrain; it does
|
||||
not replace the terrain page. The feature is seeded by the Age and saved with the written
|
||||
|
||||
@@ -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
|
||||
Stew, Experience Bottle, Ender Pearl, Feather, and Fire Charge,
|
||||
with their property chances. Black Dye and Clay Ball should not appear: the
|
||||
former had no working Legacy effect, while Generate Platform is redundant
|
||||
with default Age arrival. Without JEI, Rivencraft should still
|
||||
former had no working Legacy effect, while Clay Ball remains unsupported by
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -46,7 +46,7 @@ For a manual check, create a fresh surface Age and run `/rivencraft library near
|
||||
|
||||
Issue #75's comment calls this a despawning drop. The code differs subtly: [`EntityLinkbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/entity/EntityLinkbook.java#L190-L224) explicitly cannot despawn, but takes one starvation damage every 10,000 ticks and damage while wet. It can eventually be destroyed if left unattended; a vanilla item entity's fixed five-minute timeout is not parity. A modern equivalent should retain the visible book and avoid silently deleting its Age identity at that timeout.
|
||||
|
||||
Legacy's travel path creates no automatic Return Link. Rivencraft now defaults to `DISABLED` in new server configs while retaining `ON_CREATION` and `ALWAYS` as opt-ins. Existing saved server configs keep their explicit value. Handheld-book drop is tracked in [#85](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/issues/85), not portal-frame logic.
|
||||
Legacy's travel path creates no automatic Return Link. Rivencraft now defaults to `DISABLED` in new server configs while retaining `ON_CREATION` and `ALWAYS` as opt-ins. Existing saved server configs keep their explicit value. Issue #85 adds the active-book drop only to hand-held Linking and Descriptive Book travel. On a successful link, the exact active stack becomes a dedicated `DroppedBookEntity` at the departure point, rendered with the same open `BookModel` and textures as the book stand; a rejected link restores it to the original hand. Stand and portal use leave their books in place. The entity has unlimited vanilla lifetime and loses one of its five item-health points every 10,000 loaded ticks, plus one per wet tick, matching Legacy's slow/wet decay. Unlike Legacy's custom entity, the modern book is picked up by ordinary contact, not sneaking with an empty hand, and remains subject to ordinary item hazards. Following is not yet a Rivencraft modifier, so no Following exception is exposed or silently simulated. Both hands can activate a book, whereas Legacy's explicit stack removal tested only its selected main-hand slot.
|
||||
|
||||
## Crafting, sounds, and progression
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ release V0.6.6.0.
|
||||
| --- | --- | --- | --- |
|
||||
| Dynamic Dimensions AR Edition (required) | 0.9.1 | 0.9.1 | No newer release found on [CurseForge](https://www.curseforge.com/minecraft/mc-mods/dynamicdimension-ar-edition/files/all). |
|
||||
| Bad Packets (required) | neo-0.8.2 | neo-0.8.2 | No newer matching [Modrinth](https://modrinth.com/mod/badpackets/versions) release found. |
|
||||
| JEI (optional) | 19.57.0.449 | 19.64.0.455 | Separate update issue; verify recipe categories and GUI discovery. |
|
||||
| JEI (optional) | 19.57.0.449 at #82 review | 19.69.0.457 beta | Updated in #86 after client and dedicated GameTest passes; see [JEI review](MILESTONE_7_JEI_REVIEW.md). |
|
||||
| Create (optional) | 6.0.10 | 6.0.10 | No newer matching [Modrinth](https://modrinth.com/mod/create/versions) release found. Its jar bundles Ponder 1.0.82. |
|
||||
| Ponder (bundled with Create) | 1.0.82 | 1.0.87 library artifact | [Maven](https://maven.createmod.net/net/createmod/ponder/ponder-neoforge/maven-metadata.xml) has a newer artifact, but Create 6.0.10 still bundles 1.0.82. Do not treat this as a separately installed Create update. |
|
||||
| Immersive Portals (optional) | 6.0.7 | 6.0.7 | No newer matching [Modrinth](https://modrinth.com/mod/immersive-portals-neoforge/versions) release found. |
|
||||
| TerraBlender (optional) | 4.1.0.8 | 4.1.0.8 | No newer matching [Modrinth](https://modrinth.com/mod/terrablender/versions) release found. |
|
||||
| Serene Seasons (optional) | 10.1.0.8 compile target | 10.1.0.9 | Separate update issue; the user's Prism instance already contains 10.1.0.9, but that is not a recorded compatibility pass. |
|
||||
| Serene Seasons (optional) | 10.1.0.8 at #82 review | 10.1.0.9 | Updated in #88 after the player's full seasonal matrix passed; see [Serene review](MILESTONE_7_SERENE_REVIEW.md). |
|
||||
| GlitchCore (Serene dependency) | 2.1.0.2 | 2.1.0.2 | No newer matching [Modrinth](https://modrinth.com/mod/glitchcore/versions) release found. |
|
||||
| Biomes O' Plenty (TerraBlender test addon) | 21.1.0.14 in user's Prism instance | 21.1.0.14 | No newer matching [Modrinth](https://modrinth.com/mod/biomes-o-plenty/versions) release found. |
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# JEI compatibility review (#86)
|
||||
|
||||
Reviewed 2026-10-20 for Minecraft 1.21.1 and NeoForge 21.1.264. Rivencraft
|
||||
now targets JEI 19.69.0.457 after the client's in-game acceptance check. JEI
|
||||
remains a `compileOnly` API and an opt-in
|
||||
`localRuntime` dependency for development; the published mod has no required
|
||||
JEI dependency.
|
||||
|
||||
The [official 1.21.1 NeoForge files](https://www.curseforge.com/minecraft/mc-mods/jei/files/all?page=1&pageSize=20&version=1.21.1)
|
||||
list 19.69.0.457 (beta, 2026-10-20) after 19.64.0.455 (release, 2026-10-16).
|
||||
The [19.69.0.457 Modrinth changelog](https://modrinth.com/mod/jei/versions?g=1.21.1&l=neoforge)
|
||||
has a published-jar bytecode compatibility check. Its preceding 19.69.0.456
|
||||
build changes ingredient search to Lucene and adds a runtime ingredient-search
|
||||
refresh API. This makes page search a required client check, even though the
|
||||
current Rivencraft plugin still compiles against the 457 API. The new Maven
|
||||
runtime artifact is much larger (about 7 MB versus 2.3 MB for 455); a successful
|
||||
compile alone is not evidence that its runtime plugin works.
|
||||
|
||||
Checks performed before changing the pinned dependency:
|
||||
|
||||
| Configuration | Result |
|
||||
| --- | --- |
|
||||
| `./gradlew -Pjei_version=19.69.0.457 -PwithJei=true build` | Passed; existing JEI API uses compile with four deprecation warnings and no errors. |
|
||||
| `./gradlew -Pjei_version=19.69.0.457 -PwithJei=true runGameTestServer` | Passed; dedicated GameTest server loaded JEI 19.69.0.457 and all 74 required tests passed. |
|
||||
| `./gradlew -Pjei_version=19.69.0.457 runGameTestServer` | Passed without JEI; all 74 required tests passed. |
|
||||
| Prism client with JEI 19.69.0.457 and Rivencraft 0.6.6.6 | Client joined an integrated world; JEI registered Rivencraft extra ingredients and started without a JEI-plugin exception. The player reported an in-game pass for guide display, page search, and recipe visibility. |
|
||||
|
||||
On first 457 launch, JEI migrated its old `lowMemorySlowSearchEnabled` config
|
||||
key and saved a backup of the original `jei-client.ini`; that warning was
|
||||
handled by JEI and did not block startup. No Rivencraft config was changed.
|
||||
|
||||
The in-game acceptance report covers the visible JEI integration. A dedicated
|
||||
GameTest server proves no server-side startup failure but does not exercise the
|
||||
JEI GUI; keep page search, recipes, blank-Page hiding, and the Ink Mixer,
|
||||
Writing Desk, and Book Binder guides in future upgrade checks. JEI is not
|
||||
bundled into or required by the published Rivencraft jar.
|
||||
|
||||
The final pinned `0.6.6.7` build passed. A repeat GameTest run from the pinned
|
||||
configuration stalled during test-world preparation before running any cases;
|
||||
it was stopped. The two successful pre-pin runs above used the same JEI version
|
||||
and code, with the dependency supplied by a Gradle property override.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Serene Seasons 10.1.0.9 compatibility review (#88)
|
||||
|
||||
Reviewed 2026-10-20 for Minecraft 1.21.1 and NeoForge 21.1.264. Rivencraft
|
||||
now compiles against [Serene Seasons 10.1.0.9](https://modrinth.com/mod/serene-seasons/version/pHEgQQUE)
|
||||
with [GlitchCore 2.1.0.2](https://modrinth.com/mod/glitchcore/version/S2TfWrZR).
|
||||
Both remain optional. The Serene update fixes a precipitation-mixin recursion;
|
||||
it does not introduce a new dynamic-dimension API.
|
||||
|
||||
The 10.1.0.8 and 10.1.0.9 bytecode for Rivencraft's version-sensitive
|
||||
`SeasonsConfig`, `SeasonHandler`, `SeasonSavedData`, and `SeasonTime` integration
|
||||
classes is identical. In particular, the dimension whitelist field, per-level
|
||||
saved season clock, and season update method have unchanged signatures and
|
||||
implementations. The adapter still writes the whitelist through GlitchCore's
|
||||
config API and sends Rivencraft's whitelist update to already connected clients.
|
||||
|
||||
Verification:
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Build against 10.1.0.9 and GlitchCore 2.1.0.2 | Passed. |
|
||||
| Real Prism client with Serene 10.1.0.9, GlitchCore 2.1.0.2, and Rivencraft 0.6.6.7 | Both mods loaded and the player joined an Age. |
|
||||
| No Seasons, normal Seasons, and all four Eternal modes in new and saved Ages | Player reported the full in-game matrix passed. |
|
||||
| Reconnect, restart, and Overworld independence | Player reported the full in-game matrix passed. |
|
||||
| Isolated dedicated-server startup without Serene/GlitchCore | Reached `Done` on port 25568 with only required runtime mods, then saved and stopped. |
|
||||
| Isolated dedicated-server startup with Serene/GlitchCore | Reached `Done` on port 25569 with 10.1.0.9/2.1.0.2, then saved and stopped. |
|
||||
|
||||
Serene's sync packet is incompatible with GameTest mock players, so its
|
||||
in-game behavior must be checked with a real client and server. The seasonal
|
||||
test path in [SERENE_SEASONS_COMPATIBILITY.md](SERENE_SEASONS_COMPATIBILITY.md)
|
||||
remains the regression checklist for future upgrades. Rivencraft does not
|
||||
bundle or require Serene Seasons or GlitchCore.
|
||||
@@ -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.
|
||||
|
||||
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 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -22,7 +22,7 @@ neo_version=21.1.264
|
||||
ponder_version=1.0.82
|
||||
dynamic_dimensions_file_id=7707789
|
||||
badpackets_version=0.8.2
|
||||
jei_version=19.57.0.449
|
||||
jei_version=19.69.0.457
|
||||
# The loader version range can only use the major version of FML as bounds
|
||||
loader_version_range=[1,)
|
||||
|
||||
@@ -36,10 +36,10 @@ mod_name=Rivencraft
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=LGPL-3.0-or-later
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.6.6.2
|
||||
mod_version=0.6.8.4
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=B6nTjh4b
|
||||
serene_seasons_version_id=pHEgQQUE
|
||||
glitchcore_version_id=S2TfWrZR
|
||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||
# This should match the base package used for the mod sources.
|
||||
|
||||
@@ -54,6 +54,7 @@ public final class Rivencraft {
|
||||
ModBiomeSources.register(modEventBus);
|
||||
ModChunkGenerators.register(modEventBus);
|
||||
ModBlocks.register(modEventBus);
|
||||
net.dnorberg.rivencraft.registry.ModEntities.register(modEventBus);
|
||||
ModBlockEntities.register(modEventBus);
|
||||
ModItems.register(modEventBus);
|
||||
ModMenus.register(modEventBus);
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
|
||||
}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
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;
|
||||
return defaultBlockState().setValue(FACING, face);
|
||||
}
|
||||
@@ -97,7 +97,11 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
|
||||
LinkTarget destination = stack.is(ModItems.LINKING_BOOK.get())
|
||||
? stack.get(ModDataComponents.LINK_TARGET.get())
|
||||
: player instanceof ServerPlayer serverPlayer
|
||||
? AgeDimensionService.resolve(serverPlayer, stack.get(ModDataComponents.AGE_REFERENCE.get())).orElse(null)
|
||||
? AgeDimensionService.resolve(serverPlayer, stack.get(ModDataComponents.AGE_REFERENCE.get()),
|
||||
net.dnorberg.rivencraft.writing.OrderedWriting.EMPTY,
|
||||
!stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(),
|
||||
net.dnorberg.rivencraft.book.LinkProperties.NONE)
|
||||
.has(net.dnorberg.rivencraft.book.LinkProperties.DISABLE_PLATFORM)).orElse(null)
|
||||
: null;
|
||||
if (destination == null || server.getServer().getLevel(destination.location().dimension()) == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
|
||||
|
||||
@@ -11,7 +11,6 @@ import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
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 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();
|
||||
if (PortalTravelCooldown.blocked(player, now)) return;
|
||||
if (PortalTravelCooldown.blocked(entity, now)) return;
|
||||
if (!(level.getBlockEntity(pos) instanceof CrystalPortalBlockEntity portal)
|
||||
|| portal.receptacle() == null || !level.hasChunkAt(portal.receptacle())
|
||||
|| !(level.getBlockEntity(portal.receptacle()) instanceof BookReceptacleBlockEntity holder)) return;
|
||||
ItemStack book = holder.getBook();
|
||||
if (book.isEmpty()) return;
|
||||
PortalTravelCooldown.mark(player, now);
|
||||
PortalTravelQueue.enqueue(player, server, pos, portal.receptacle(), book, now);
|
||||
PortalTravelCooldown.mark(entity, now);
|
||||
PortalTravelQueue.enqueue(entity, server, pos, portal.receptacle(), book, now);
|
||||
}
|
||||
|
||||
@Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbor,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.Containers;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class LinkModifierBlock extends BaseEntityBlock {
|
||||
public static final MapCodec<LinkModifierBlock> CODEC = simpleCodec(LinkModifierBlock::new);
|
||||
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
|
||||
|
||||
public LinkModifierBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block,
|
||||
BlockState> builder) { builder.add(FACING); }
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
|
||||
}
|
||||
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new LinkModifierBlockEntity(pos, state);
|
||||
}
|
||||
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer
|
||||
&& level.getBlockEntity(pos) instanceof LinkModifierBlockEntity modifier) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new LinkModifierMenu(id, inventory, modifier),
|
||||
Component.translatable("container.rivencraft.link_modifier")),
|
||||
buffer -> buffer.writeBlockPos(pos));
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
@Override protected void onRemove(BlockState state, Level level, BlockPos pos,
|
||||
BlockState replacement, boolean moving) {
|
||||
if (!state.is(replacement.getBlock()) && !level.isClientSide
|
||||
&& level.getBlockEntity(pos) instanceof LinkModifierBlockEntity modifier) {
|
||||
Containers.dropContents(level, pos, modifier);
|
||||
}
|
||||
super.onRemove(state, level, pos, replacement, moving);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
@@ -96,7 +97,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
case COVER -> validCover(stack);
|
||||
case LINK_PANEL -> stack.is(ModItems.PAGE.get())
|
||||
&& PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL;
|
||||
case NOTEBOOK -> stack.is(ModItems.NOTEBOOK.get());
|
||||
case NOTEBOOK -> CollationFolderItem.isCollection(stack);
|
||||
default -> slot >= FIRST_PAGE && slot < SIZE && validPage(stack);
|
||||
};
|
||||
}
|
||||
@@ -296,7 +297,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
|
||||
/** Import a Notebook from either its stored slot or the player's cursor. */
|
||||
public ItemStack importNotebookFromStack(ItemStack notebook, int index) {
|
||||
if (!notebook.is(ModItems.NOTEBOOK.get()) || notebook.getCount() != 1) return ItemStack.EMPTY;
|
||||
if (!CollationFolderItem.isCollection(notebook) || notebook.getCount() != 1) return ItemStack.EMPTY;
|
||||
List<ItemStack> pages = notebook.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
if (pages.isEmpty()) return ItemStack.EMPTY;
|
||||
int end = nextPageIndex();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.ContainerHelper;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** One persisted book slot; edits are performed by the server menu. */
|
||||
public final class LinkModifierBlockEntity extends BlockEntity implements Container {
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(1, ItemStack.EMPTY);
|
||||
|
||||
public LinkModifierBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.LINK_MODIFIER.get(), pos, state);
|
||||
}
|
||||
|
||||
@Override public int getContainerSize() { return 1; }
|
||||
@Override public boolean isEmpty() { return items.getFirst().isEmpty(); }
|
||||
@Override public ItemStack getItem(int slot) { return items.get(slot); }
|
||||
@Override public ItemStack removeItem(int slot, int amount) {
|
||||
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
|
||||
if (!removed.isEmpty()) setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public ItemStack removeItemNoUpdate(int slot) {
|
||||
ItemStack removed = ContainerHelper.takeItem(items, slot);
|
||||
if (!removed.isEmpty()) setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public void setItem(int slot, ItemStack stack) {
|
||||
items.set(slot, stack.copyWithCount(Math.min(1, stack.getCount())));
|
||||
setChanged();
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
return slot == 0 && (stack.is(ModItems.LINKING_BOOK.get())
|
||||
|| stack.is(ModItems.DESCRIPTIVE_BOOK.get()));
|
||||
}
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return level != null && level.isLoaded(worldPosition) && level.getBlockEntity(worldPosition) == this
|
||||
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
|
||||
worldPosition.getZ() + 0.5) <= 64.0;
|
||||
}
|
||||
@Override public void clearContent() {
|
||||
items = NonNullList.withSize(1, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
}
|
||||
@Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.loadAdditional(tag, registries);
|
||||
items = NonNullList.withSize(1, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(tag, items, registries);
|
||||
}
|
||||
@Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.saveAdditional(tag, registries);
|
||||
ContainerHelper.saveAllItems(tag, items, registries);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.InkVialExchange;
|
||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
@@ -87,7 +88,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
|
||||
worldPosition.getZ() + 0.5) <= 64.0;
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
if (slot < COLLECTIONS) return stack.is(ModItems.NOTEBOOK.get())
|
||||
if (slot < COLLECTIONS) return CollationFolderItem.isCollection(stack)
|
||||
&& stack.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY)
|
||||
.pages().stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL)
|
||||
|| stack.is(ModItems.PAGE.get())
|
||||
|
||||
@@ -118,4 +118,10 @@ public record AgeDefinition(
|
||||
createdGameTime, dimensionId, spawn.immutable(), traits, plan, seasonMode,
|
||||
randomFeatures, instability, instabilityPlan);
|
||||
}
|
||||
|
||||
/** A frozen authored profile takes precedence over the profile ID on old books. */
|
||||
public boolean caveTerrain() {
|
||||
return AgeBiomeSelection.isCave(plan == null
|
||||
? AgeRuntimeProfiles.resolve(generationProfile) : plan.runtimeProfile());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Retires an exact Age and moves its unloaded region data out of the live world. */
|
||||
public final class AgeDeletionService {
|
||||
public enum Result { RETIRED, ALREADY_DEAD, OCCUPIED, UNLOAD_FAILED, STORAGE_FAILED }
|
||||
|
||||
private AgeDeletionService() { }
|
||||
|
||||
public static Result retire(MinecraftServer server, AgeDefinition age) {
|
||||
AgeRegistrySavedData registry = AgeRegistrySavedData.get(server.overworld());
|
||||
if (registry.isDead(age.id())) return Result.ALREADY_DEAD;
|
||||
var level = server.getLevel(AgeDimensionService.dimensionKey(age));
|
||||
if (level != null && !level.players().isEmpty()) return Result.OCCUPIED;
|
||||
if (AgeDimensionService.unload(server, age) == AgeDimensionService.UnloadResult.FAILED)
|
||||
return Result.UNLOAD_FAILED;
|
||||
|
||||
Path world = server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
Path ageRoot = world.resolve("dimensions/rivencraft/age").normalize();
|
||||
Path data = world.resolve("dimensions")
|
||||
.resolve(age.dimensionId().getNamespace())
|
||||
.resolve(age.dimensionId().getPath()).normalize();
|
||||
// Exact Rivencraft Age only; never move the shared age/ directory.
|
||||
if (!data.startsWith(ageRoot) || data.equals(ageRoot)
|
||||
|| !"rivencraft".equals(age.dimensionId().getNamespace())
|
||||
|| !age.dimensionId().getPath().startsWith("age/")) return Result.STORAGE_FAILED;
|
||||
try {
|
||||
if (Files.isSymbolicLink(world.resolve("dimensions"))
|
||||
|| Files.isSymbolicLink(world.resolve("dimensions/rivencraft"))
|
||||
|| Files.isSymbolicLink(ageRoot) || Files.isSymbolicLink(data)) return Result.STORAGE_FAILED;
|
||||
if (Files.exists(data, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Path archive = world.getParent().resolve(world.getFileName() + "-retired-ages")
|
||||
.resolve(age.id().toString());
|
||||
if (Files.exists(archive, LinkOption.NOFOLLOW_LINKS)) return Result.STORAGE_FAILED;
|
||||
Files.createDirectories(archive.getParent());
|
||||
try { Files.move(data, archive, StandardCopyOption.ATOMIC_MOVE); }
|
||||
catch (AtomicMoveNotSupportedException exception) { Files.move(data, archive); }
|
||||
Rivencraft.LOGGER.warn("Retired Age {} data moved from {} to {}", age.id(), data, archive);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
Rivencraft.LOGGER.error("Cannot archive data for Age {}", age.id(), exception);
|
||||
return Result.STORAGE_FAILED;
|
||||
}
|
||||
registry.markDead(age.id());
|
||||
return Result.RETIRED;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ public final class AgeDimensionService {
|
||||
DynamicDimensionLoadCallback.register((server, loader) -> {
|
||||
ServerLevel overworld = server.overworld();
|
||||
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
|
||||
if (AgeRegistrySavedData.get(overworld).isDead(age.id())) continue;
|
||||
if (!canLoadProfile(server, age)) continue;
|
||||
ServerLevel level = loader.loadDynamicDimension(
|
||||
age.dimensionId(),
|
||||
@@ -95,8 +96,19 @@ public final class AgeDimensionService {
|
||||
|
||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
|
||||
OrderedWriting writing) {
|
||||
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference, writing);
|
||||
ServerLevel level = ensureLoaded(player.getServer(), definition);
|
||||
return resolve(player, reference, writing, true);
|
||||
}
|
||||
|
||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
|
||||
OrderedWriting writing, boolean generatePlatform) {
|
||||
return resolve(player.serverLevel(), reference, writing, generatePlatform);
|
||||
}
|
||||
|
||||
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) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -104,7 +116,8 @@ public final class AgeDimensionService {
|
||||
BlockPos spawn = definition.spawn();
|
||||
if (spawn == null) {
|
||||
applyInitialCelestialPhase(level, definition);
|
||||
spawn = createSafeSpawn(level, definition);
|
||||
spawn = createSafeSpawn(level, definition, generatePlatform);
|
||||
if (spawn == null) return Optional.empty();
|
||||
if (AgeFeatureSelection.resolved(definition.plan()).stream()
|
||||
.anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES))
|
||||
|| definition.randomFeatures().stream()
|
||||
@@ -122,6 +135,7 @@ public final class AgeDimensionService {
|
||||
}
|
||||
|
||||
public static ServerLevel ensureLoaded(MinecraftServer server, AgeDefinition definition) {
|
||||
if (AgeRegistrySavedData.get(server.overworld()).isDead(definition.id())) return null;
|
||||
ResourceKey<Level> key = dimensionKey(definition);
|
||||
ServerLevel existing = server.getLevel(key);
|
||||
if (existing != null) {
|
||||
@@ -170,6 +184,10 @@ public final class AgeDimensionService {
|
||||
}
|
||||
|
||||
private static void applyCelestialClock(ServerLevel level, AgeDefinition age) {
|
||||
if (age.caveTerrain()) {
|
||||
level.setDayTimePerTick(1.0F);
|
||||
return;
|
||||
}
|
||||
if (age.traits().hasStoppedCelestialCycle()) {
|
||||
// NeoForge rejects an exact zero rate. The smallest positive float
|
||||
// never advances the integer clock during a practical world
|
||||
@@ -198,6 +216,7 @@ public final class AgeDimensionService {
|
||||
}
|
||||
|
||||
private static void applyInitialCelestialPhase(ServerLevel level, AgeDefinition age) {
|
||||
if (age.caveTerrain()) return;
|
||||
long phaseTime = (long)(age.traits().celestialPhase() * Level.TICKS_PER_DAY);
|
||||
if (AgeEnvironmentCompiler.hasWrittenPhase(age.plan())) {
|
||||
// Legacy's Nadir is midnight, while vanilla day time zero is sunrise.
|
||||
@@ -216,21 +235,24 @@ public final class AgeDimensionService {
|
||||
return ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, definition.dimensionId());
|
||||
}
|
||||
|
||||
private static BlockPos createSafeSpawn(ServerLevel level, AgeDefinition age) {
|
||||
private static BlockPos createSafeSpawn(ServerLevel level, AgeDefinition age, boolean generatePlatform) {
|
||||
BlockPos floorCenter;
|
||||
if (age.generationProfile().equals(AgeGenerationProfiles.CAVE)) {
|
||||
if (age.caveTerrain()) {
|
||||
// Cave Ages are enclosed. Searching from the world ceiling would
|
||||
// select or expose their roof, so begin with the floor beneath a
|
||||
// player standing at Y=64 and search downward from there.
|
||||
floorCenter = highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1)
|
||||
.orElseGet(() -> fallbackSpawnFloor(level));
|
||||
.orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null);
|
||||
} else if (isNetherStyle(level)) {
|
||||
floorCenter = netherSpawnFloor(level);
|
||||
floorCenter = generatePlatform ? netherSpawnFloor(level)
|
||||
: highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1).orElse(null);
|
||||
} else {
|
||||
floorCenter = highestFootedSurface(level).orElseGet(() -> fallbackSpawnFloor(level));
|
||||
floorCenter = highestFootedSurface(level).orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null);
|
||||
}
|
||||
|
||||
for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) {
|
||||
if (floorCenter == null) return null;
|
||||
|
||||
if (generatePlatform) for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) {
|
||||
for (int z = -PLATFORM_RADIUS; z <= PLATFORM_RADIUS; z++) {
|
||||
BlockPos floor = floorCenter.offset(x, 0, z);
|
||||
level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 2);
|
||||
@@ -241,6 +263,11 @@ public final class AgeDimensionService {
|
||||
}
|
||||
|
||||
BlockPos spawn = floorCenter.above();
|
||||
if (!generatePlatform) {
|
||||
var safe = LinkingService.resolveDestination(level, spawn, true);
|
||||
if (safe.isEmpty()) return null;
|
||||
spawn = BlockPos.containing(safe.get());
|
||||
}
|
||||
level.setDefaultSpawnPos(spawn, 0.0F);
|
||||
return spawn;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,23 @@ public final class AgeEnvironmentCompiler {
|
||||
|
||||
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
|
||||
if (plan == null) return seeded;
|
||||
AgeTraits applied;
|
||||
if (plan.version() >= 4) {
|
||||
AgeTraits base = applyLegacy(plan, seeded);
|
||||
CelestialWriting ordered = orderedCelestial(plan, seeded);
|
||||
return new AgeTraits(base.biomeDistribution(), base.lighting(), base.weather(),
|
||||
applied = new AgeTraits(base.biomeDistribution(), base.lighting(), base.weather(),
|
||||
ordered.period(), base.celestialAngle(), ordered.phase());
|
||||
} else {
|
||||
applied = applyLegacy(plan, seeded);
|
||||
}
|
||||
return applyLegacy(plan, seeded);
|
||||
return forTerrain(applied, plan.runtimeProfile());
|
||||
}
|
||||
|
||||
/** Cave terrain has no exposed weather or meaningful sky clock, even with authored pages. */
|
||||
public static AgeTraits forTerrain(AgeTraits traits, AgeRuntimeProfile profile) {
|
||||
if (!AgeBiomeSelection.isCave(profile)) return traits;
|
||||
return new AgeTraits(traits.biomeDistribution(), traits.lighting(), AgeTraits.Weather.OFF,
|
||||
1.0D, 0.0F, 0.0F);
|
||||
}
|
||||
|
||||
/** Version-3 plans keep their frozen, stage-sorted environment interpretation. */
|
||||
|
||||
@@ -15,6 +15,7 @@ import net.minecraft.resources.ResourceLocation;
|
||||
public record AgeInstability(int version, int score, List<Contribution> breakdown) {
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
private static final int BRIGHT_LIGHTING_COST = 500;
|
||||
private static final int INEFFECTIVE_CAVE_PAGE_COST = 100;
|
||||
private static final int EXCESS_LEGACY_STRUCTURE_COST = 100;
|
||||
private static final int FORCED_STRUCTURE_BIOME_COST = 100;
|
||||
private static final Map<ResourceLocation, String> COUNTED_STRUCTURES = Map.ofEntries(
|
||||
@@ -88,7 +89,19 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
||||
private static void addAuditedSymbolCosts(AgeGenerationPlan plan, RegistryAccess registries,
|
||||
List<Contribution> contributions) {
|
||||
Map<ResourceLocation, Integer> structureCounts = new HashMap<>();
|
||||
boolean cave = AgeBiomeSelection.isCave(plan.runtimeProfile());
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
if (cave) {
|
||||
var role = symbol.semantics().role();
|
||||
if (switch (role) {
|
||||
case WEATHER, SEASON, CELESTIAL, PERIOD_MODIFIER, PHASE_MODIFIER, ANGLE_MODIFIER -> true;
|
||||
default -> false;
|
||||
}) {
|
||||
contributions.add(new Contribution("cave_ignored_" + role.id(), symbol.index(),
|
||||
INEFFECTIVE_CAVE_PAGE_COST,
|
||||
"Sky or weather page has no effect in Cave terrain"));
|
||||
}
|
||||
}
|
||||
if (symbol.semantics().role()
|
||||
== net.dnorberg.rivencraft.writing.SymbolSemantics.Role.ENVIRONMENT
|
||||
&& symbol.semantics().value()
|
||||
|
||||
@@ -23,6 +23,49 @@ public final class AgePersistenceGameTests {
|
||||
private AgePersistenceGameTests() {
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void modifierSeedChangePersistsWithoutChangingAgeIdentity(GameTestHelper helper) {
|
||||
UUID id = UUID.randomUUID();
|
||||
AgeDefinition original = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(id, "Reseed " + id));
|
||||
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
|
||||
long newSeed = original.seed() == 987654321L ? 987654322L : 987654321L;
|
||||
helper.assertTrue(registry.reseed(helper.getLevel().getServer(), id, newSeed),
|
||||
"An unoccupied Age must accept a new seed");
|
||||
AgeDefinition changed = registry.find(id).orElseThrow();
|
||||
helper.assertValueEqual(changed.id(), original.id(), "Reseeding must preserve the Age identity");
|
||||
helper.assertValueEqual(changed.dimensionId(), original.dimensionId(),
|
||||
"Reseeding must preserve its dimension and existing chunk location");
|
||||
helper.assertValueEqual(changed.seed(), newSeed, "The new generator seed must be saved");
|
||||
AgeDefinition restored = AgeRegistrySavedData.load(
|
||||
registry.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess()).find(id).orElseThrow();
|
||||
helper.assertValueEqual(restored.seed(), newSeed, "The new seed must survive reload");
|
||||
var ageLevel = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), changed);
|
||||
helper.assertTrue(ageLevel != null, "The reseeded Age must reload");
|
||||
helper.assertValueEqual(ageLevel.getSeed(), newSeed,
|
||||
"The runtime level must use the saved seed for new chunks");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void retiredAgeIdentitySurvivesSaveAndCannotLoad(GameTestHelper helper) {
|
||||
UUID id = UUID.randomUUID();
|
||||
AgeReference reference = new AgeReference(id, "Retired " + id);
|
||||
AgeDefinition age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), reference);
|
||||
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
|
||||
helper.assertTrue(registry.markDead(id), "The first retirement must set the tombstone");
|
||||
helper.assertTrue(!registry.markDead(id), "Retirement must be idempotent");
|
||||
helper.assertTrue(AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), age) == null,
|
||||
"A retired Age must never load from an old book");
|
||||
AgeRegistrySavedData restored = AgeRegistrySavedData.load(
|
||||
registry.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess());
|
||||
helper.assertTrue(restored.isDead(id) && restored.find(id).isPresent(),
|
||||
"Both the retired flag and reserved Age identity must survive reload");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredPlanRetainsResolvedMeaningAndAgeIdentity(GameTestHelper helper) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
|
||||
@@ -5,6 +5,9 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
@@ -13,6 +16,7 @@ import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.neoforged.fml.ModList;
|
||||
@@ -21,6 +25,7 @@ import net.neoforged.fml.ModList;
|
||||
public final class AgeRegistrySavedData extends SavedData {
|
||||
private static final String DATA_NAME = "rivencraft_ages";
|
||||
private static final String AGES_TAG = "Ages";
|
||||
private static final String DEAD_AGES_TAG = "DeadAges";
|
||||
private static final String ID_TAG = "Id";
|
||||
private static final String NAME_TAG = "Name";
|
||||
private static final String SEED_TAG = "Seed";
|
||||
@@ -50,6 +55,7 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
);
|
||||
|
||||
private final Map<UUID, AgeDefinition> ages = new HashMap<>();
|
||||
private final Set<UUID> deadAges = new HashSet<>();
|
||||
|
||||
public static AgeRegistrySavedData get(ServerLevel level) {
|
||||
return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME);
|
||||
@@ -110,6 +116,16 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
return List.copyOf(ages.values());
|
||||
}
|
||||
|
||||
public boolean isDead(UUID ageId) { return deadAges.contains(ageId); }
|
||||
|
||||
/** Keep the ID and definition reserved so an old book cannot regenerate a retired Age. */
|
||||
public boolean markDead(UUID ageId) {
|
||||
if (!ages.containsKey(ageId)) return false;
|
||||
boolean changed = deadAges.add(ageId);
|
||||
if (changed) setDirty();
|
||||
return changed;
|
||||
}
|
||||
|
||||
public AgeDefinition setSpawn(UUID ageId, BlockPos spawn) {
|
||||
AgeDefinition existing = ages.get(ageId);
|
||||
if (existing == null) {
|
||||
@@ -121,6 +137,32 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** Changes the generator for future chunks; existing chunk data is deliberately preserved. */
|
||||
public boolean reseed(MinecraftServer server, UUID ageId, long seed) {
|
||||
AgeDefinition previous = ages.get(ageId);
|
||||
if (previous == null || isDead(ageId)) return false;
|
||||
if (previous.seed() == seed) return true;
|
||||
ServerLevel loaded = server.getLevel(AgeDimensionService.dimensionKey(previous));
|
||||
if (loaded != null && !loaded.players().isEmpty()) return false;
|
||||
|
||||
// Legacy changes the Age's seed, not its already-written pages or selected traits.
|
||||
// Keeping the frozen plan and feature list prevents a visited Age from changing
|
||||
// its identity while newly generated chunks use the revised terrain noise.
|
||||
AgeGenerationPlan oldPlan = previous.plan();
|
||||
AgeGenerationPlan plan = oldPlan == null ? null : new AgeGenerationPlan(oldPlan.version(), seed,
|
||||
oldPlan.writing(), oldPlan.symbols(), oldPlan.runtimeProfile(), oldPlan.biomeSelection(),
|
||||
oldPlan.activeSymbolIndices(), oldPlan.writingInstability());
|
||||
AgeDefinition changed = new AgeDefinition(previous.id(), previous.name(), seed,
|
||||
previous.generationProfile(), previous.generationVersion(), previous.createdGameTime(),
|
||||
previous.dimensionId(), previous.spawn(), previous.traits(), plan, previous.seasonMode(),
|
||||
previous.randomFeatures(), previous.instability(), previous.instabilityPlan());
|
||||
if (AgeDimensionService.unload(server, previous) == AgeDimensionService.UnloadResult.FAILED)
|
||||
return false;
|
||||
ages.put(ageId, changed);
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
private AgeDefinition getOrCreate(AgeReference reference, OrderedWriting writing,
|
||||
long worldSeed, long createdGameTime, RegistryAccess registries) {
|
||||
AgeDefinition existing = ages.get(reference.id());
|
||||
@@ -134,7 +176,9 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
ResourceLocation seedProfile = AgeRuntimeProfiles.selectForWriting(seed, writing, registries);
|
||||
AgeTraits traits = AgeTraits.select(seed);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, seed, seedProfile, traits, registries);
|
||||
AgeTraits effectiveTraits = AgeEnvironmentCompiler.apply(plan, traits);
|
||||
AgeTraits effectiveTraits = plan == null
|
||||
? AgeEnvironmentCompiler.forTerrain(traits, AgeRuntimeProfiles.resolve(seedProfile))
|
||||
: AgeEnvironmentCompiler.apply(plan, traits);
|
||||
AgeInstability instability = AgeInstability.compute(plan, effectiveTraits, registries);
|
||||
AgeInstabilityPlan instabilityPlan = AgeInstabilityPlan.select(instability, seed);
|
||||
AgeDefinition created = new AgeDefinition(
|
||||
@@ -148,7 +192,8 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
null,
|
||||
effectiveTraits,
|
||||
plan,
|
||||
AgeSeasonMode.select(plan, seed, ModList.get().isLoaded("sereneseasons")),
|
||||
AgeSeasonMode.select(plan, seed, ModList.get().isLoaded("sereneseasons"),
|
||||
plan == null ? AgeRuntimeProfiles.resolve(seedProfile) : plan.runtimeProfile()),
|
||||
plan == null ? AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(seedProfile), registries) : List.of(),
|
||||
instability,
|
||||
@@ -161,6 +206,11 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
|
||||
static AgeRegistrySavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AgeRegistrySavedData data = new AgeRegistrySavedData();
|
||||
ListTag deadTags = tag.getList(DEAD_AGES_TAG, StringTag.TAG_STRING);
|
||||
for (int index = 0; index < deadTags.size(); index++) {
|
||||
try { data.deadAges.add(UUID.fromString(deadTags.getString(index))); }
|
||||
catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
ListTag serializedAges = tag.getList(AGES_TAG, CompoundTag.TAG_COMPOUND);
|
||||
for (int index = 0; index < serializedAges.size(); index++) {
|
||||
CompoundTag entry = serializedAges.getCompound(index);
|
||||
@@ -264,6 +314,9 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
serializedAges.add(entry);
|
||||
});
|
||||
tag.put(AGES_TAG, serializedAges);
|
||||
ListTag deadTags = new ListTag();
|
||||
deadAges.stream().sorted().forEach(id -> deadTags.add(StringTag.valueOf(id.toString())));
|
||||
tag.put(DEAD_AGES_TAG, deadTags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ public enum AgeSeasonMode {
|
||||
|
||||
/** An omitted page is selected once when an Age is created with Serene installed. */
|
||||
public static AgeSeasonMode select(AgeGenerationPlan plan, long ageSeed, boolean sereneInstalled) {
|
||||
return select(plan, ageSeed, sereneInstalled, plan == null ? null : plan.runtimeProfile());
|
||||
}
|
||||
|
||||
public static AgeSeasonMode select(AgeGenerationPlan plan, long ageSeed, boolean sereneInstalled,
|
||||
AgeRuntimeProfile profile) {
|
||||
if (profile != null && AgeBiomeSelection.isCave(profile)) return NO_SEASONS;
|
||||
if (hasAuthoredPage(plan)) return from(plan);
|
||||
return sereneInstalled ? LegacyRankChoice.choose(new Random(ageSeed ^ RANDOM_SALT), List.of(
|
||||
new LegacyRankChoice.Option<>(SEASONS, 1),
|
||||
@@ -31,6 +37,7 @@ public enum AgeSeasonMode {
|
||||
|
||||
/** Existing Ages without a stored choice retain their old cycling behavior. */
|
||||
public static AgeSeasonMode from(AgeDefinition age) {
|
||||
if (age.caveTerrain()) return NO_SEASONS;
|
||||
return age.seasonMode() == null ? from(age.plan()) : age.seasonMode();
|
||||
}
|
||||
|
||||
@@ -49,6 +56,7 @@ public enum AgeSeasonMode {
|
||||
|
||||
public static AgeSeasonMode from(AgeGenerationPlan plan) {
|
||||
if (plan == null) return SEASONS;
|
||||
if (AgeBiomeSelection.isCave(plan.runtimeProfile())) return NO_SEASONS;
|
||||
return plan.activeSymbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SEASON)
|
||||
.map(symbol -> valueOf(((SymbolSemantics.ChoiceValue) symbol.semantics().value())
|
||||
@@ -58,6 +66,7 @@ public enum AgeSeasonMode {
|
||||
|
||||
public static void validate(AgeGenerationPlan plan) {
|
||||
if (plan == null) return;
|
||||
if (AgeBiomeSelection.isCave(plan.runtimeProfile())) return;
|
||||
if (hasAuthoredPage(plan) && from(plan) != NO_SEASONS
|
||||
&& !ModList.get().isLoaded("sereneseasons")) {
|
||||
throw new IllegalArgumentException("This seasonal page requires Serene Seasons to create the Age");
|
||||
|
||||
@@ -26,7 +26,7 @@ public final class AgeWeatherService {
|
||||
return;
|
||||
}
|
||||
|
||||
AgeTraits.Weather weather = age.traits().weather();
|
||||
AgeTraits.Weather weather = age.caveTerrain() ? AgeTraits.Weather.OFF : age.traits().weather();
|
||||
if (weather == AgeTraits.Weather.NORMAL) {
|
||||
STATES.remove(level);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Focused Cave terrain writing and persisted-generation coverage for issue #14. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class CaveTerrainGameTests {
|
||||
private CaveTerrainGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void caveSkyAndWeatherPagesCostInstabilityButDoNotApply(GameTestHelper helper) {
|
||||
OrderedWriting cave = writing("terrain/cave", "biome/lush_caves", "biome/nether_wastes",
|
||||
"biome/tiled", "weather/storm", "season/eternal_winter", "period/zero",
|
||||
"celestial/normal_time");
|
||||
AgeTraits seeded = new AgeTraits(AgeTraits.BiomeDistribution.SINGLE, AgeTraits.Lighting.NORMAL,
|
||||
AgeTraits.Weather.ALWAYS, 0.5D, 0.0F, 0.25F);
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(cave, 42L, AgeGenerationProfiles.CAVE,
|
||||
seeded, registries);
|
||||
AgeTraits applied = AgeEnvironmentCompiler.apply(plan, seeded);
|
||||
helper.assertValueEqual(applied.weather(), AgeTraits.Weather.OFF, "Cave weather must be off");
|
||||
helper.assertValueEqual(applied.celestialPeriod(), 1.0D, "Cave time pages must not alter the clock");
|
||||
helper.assertValueEqual(applied.celestialPhase(), 0.0F, "Cave time pages must not set a phase");
|
||||
helper.assertValueEqual(AgeSeasonMode.select(plan, 42L, true), AgeSeasonMode.NO_SEASONS,
|
||||
"Cave seasons must be disabled even when Serene Seasons is present");
|
||||
helper.assertValueEqual(plan.biomeSelection().biomes(),
|
||||
List.of(Biomes.LUSH_CAVES.location(), Biomes.NETHER_WASTES.location()),
|
||||
"Cave writing must retain exactly its cave and Nether biomes");
|
||||
AgeInstability instability = AgeInstability.compute(plan, applied, registries);
|
||||
helper.assertValueEqual(instability.breakdown().stream()
|
||||
.filter(part -> part.reason().startsWith("cave_ignored_")).count(), 4L,
|
||||
"Each ineffective weather, season, period, and celestial page must add instability");
|
||||
helper.assertValueEqual(AgeInstability.compute(AgeGenerationPlan.load(plan.save()), applied, registries),
|
||||
instability, "Cave page penalties must survive plan reload");
|
||||
|
||||
AgeGenerationPlan surfaceCaves = AgeGenerationPlan.compile(writing("terrain/standard", "feature/caves"),
|
||||
42L, AgeGenerationProfiles.STANDARD, seeded, registries);
|
||||
helper.assertTrue(!AgeBiomeSelection.isCave(surfaceCaves.runtimeProfile())
|
||||
&& AgeFeatureSelection.resolved(surfaceCaves).stream()
|
||||
.anyMatch(choice -> AgeFeatureSelection.caves(choice.id())),
|
||||
"The separate Caves feature page must still add caves to surface terrain");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty", timeoutTicks = 600)
|
||||
public static void authoredCaveBiomeAndSpawnSurviveSavedDataReload(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
AgeReference reference = new AgeReference(UUID.randomUUID(), "Cave issue 14 " + UUID.randomUUID());
|
||||
AgeDimensionService.resolve(player, reference,
|
||||
writing("terrain/cave", "biome/nether_wastes", "biome/single")).orElseThrow();
|
||||
var registry = AgeRegistrySavedData.get(helper.getLevel());
|
||||
AgeDefinition age = registry.find(reference.id()).orElseThrow();
|
||||
helper.assertTrue(age.caveTerrain(), "The Age must retain Cave terrain");
|
||||
helper.assertTrue(age.spawn() != null && age.spawn().getY() <= 64,
|
||||
"Cave spawn search must start at Y=64, not atop the roof");
|
||||
helper.assertValueEqual(age.traits().weather(), AgeTraits.Weather.OFF,
|
||||
"A new Cave Age must persist disabled weather");
|
||||
helper.assertValueEqual(age.seasonMode(), AgeSeasonMode.NO_SEASONS,
|
||||
"A new Cave Age must persist disabled seasons");
|
||||
AgeDefinition reloaded = AgeRegistrySavedData.load(
|
||||
registry.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess()).find(reference.id()).orElseThrow();
|
||||
helper.assertValueEqual(reloaded, age, "Saved Cave biome, spawn, and environment must survive reload");
|
||||
helper.assertValueEqual(reloaded.plan().biomeSelection().biomes(),
|
||||
List.of(Biomes.NETHER_WASTES.location()),
|
||||
"The generated Cave Age must preserve its authored-only Nether pool");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static OrderedWriting writing(String... pages) {
|
||||
var entries = new java.util.ArrayList<WritingEntry>();
|
||||
entries.add(WritingEntry.linkPanel());
|
||||
for (String page : pages) entries.add(WritingEntry.symbol(new SymbolReference(
|
||||
ResourceLocation.parse("rivencraft:" + page))));
|
||||
return new OrderedWriting(entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.dnorberg.rivencraft.registry.ModEntities;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
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.nbt.CompoundTag;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
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.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class HandheldBookTravelGameTests {
|
||||
private HandheldBookTravelGameTests() {
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void successfulHandheldTravelLeavesOnlyActiveBookAtOrigin(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, book);
|
||||
player.getInventory().setItem(1, new ItemStack(Items.DIAMOND));
|
||||
var source = player.position();
|
||||
BlockPos destination = helper.absolutePos(new BlockPos(2, 1, 2));
|
||||
helper.getLevel().setBlock(destination, Blocks.AIR.defaultBlockState(), 2);
|
||||
helper.getLevel().setBlock(destination.above(), Blocks.AIR.defaultBlockState(), 2);
|
||||
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), destination), 0, 0);
|
||||
|
||||
helper.assertValueEqual(LinkingService.travelHandheld(player, InteractionHand.MAIN_HAND, book, target,
|
||||
new LinkProperties(LinkProperties.INTRA_LINKING), false), LinkingService.LinkResult.SUCCESS,
|
||||
"A valid intra-dimensional handheld link must succeed");
|
||||
helper.assertTrue(player.getMainHandItem().isEmpty(), "Only the active book must leave the hand");
|
||||
helper.assertTrue(player.getInventory().getItem(1).is(Items.DIAMOND),
|
||||
"An unrelated inventory item must remain with the player");
|
||||
var drops = helper.getLevel().getEntitiesOfClass(DroppedBookEntity.class, new AABB(source, source).inflate(2.0),
|
||||
entity -> entity.getItem().is(ModItems.LINKING_BOOK.get()));
|
||||
helper.assertValueEqual(drops.size(), 1, "Exactly one recoverable book must remain at the departure point");
|
||||
helper.assertTrue(drops.getFirst().getItem() == book,
|
||||
"The model-rendered entity must retain the exact active book stack");
|
||||
helper.assertTrue(drops.getFirst().copy() instanceof DroppedBookEntity,
|
||||
"The pickup-animation copy must keep the custom renderer's entity type");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void rejectedHandheldTravelKeepsBook(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, book);
|
||||
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), player.blockPosition()), 0, 0);
|
||||
|
||||
helper.assertValueEqual(LinkingService.travelHandheld(player, InteractionHand.OFF_HAND, book, target,
|
||||
LinkProperties.NONE, false), LinkingService.LinkResult.INTRA_LINKING_REQUIRED,
|
||||
"A same-dimension link without Intra Linking must be rejected");
|
||||
helper.assertTrue(player.getOffhandItem() == book, "The exact rejected book must be restored to its hand");
|
||||
helper.assertTrue(helper.getLevel().getEntitiesOfClass(ItemEntity.class,
|
||||
new AABB(player.position(), player.position()).inflate(2.0),
|
||||
entity -> entity.getItem().is(ModItems.LINKING_BOOK.get())).isEmpty(),
|
||||
"A rejected link must not leave a second copy at the origin");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void disarmDropsOtherInventoryWithoutDuplicatingActiveBook(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, book);
|
||||
player.getInventory().setItem(1, new ItemStack(Items.DIAMOND));
|
||||
var source = player.position();
|
||||
BlockPos destination = helper.absolutePos(new BlockPos(2, 1, 2));
|
||||
helper.getLevel().setBlock(destination, Blocks.AIR.defaultBlockState(), 2);
|
||||
helper.getLevel().setBlock(destination.above(), Blocks.AIR.defaultBlockState(), 2);
|
||||
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), destination), 0, 0);
|
||||
|
||||
helper.assertValueEqual(LinkingService.travelHandheld(player, InteractionHand.MAIN_HAND, book, target,
|
||||
new LinkProperties(LinkProperties.INTRA_LINKING | LinkProperties.DISARM), false),
|
||||
LinkingService.LinkResult.SUCCESS, "Disarm must not block handheld travel");
|
||||
var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(source, source).inflate(2.0));
|
||||
helper.assertValueEqual(drops.stream().filter(entity -> entity instanceof DroppedBookEntity).count(),
|
||||
1L, "Disarm must not create a second copy of the active book");
|
||||
helper.assertValueEqual(drops.stream().filter(entity -> entity.getItem().is(Items.DIAMOND)).count(),
|
||||
1L, "Disarm must still drop unrelated inventory items");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void originBookUsesSlowDecayWithoutVanillaDespawn(GameTestHelper helper) {
|
||||
DroppedBookEntity book = new DroppedBookEntity(helper.getLevel(),
|
||||
net.minecraft.world.phys.Vec3.ZERO, new ItemStack(ModItems.LINKING_BOOK.get()));
|
||||
CompoundTag seeded = new CompoundTag();
|
||||
book.saveWithoutId(seeded);
|
||||
seeded.putInt("BookDecayTicks", 9_999);
|
||||
book.readAdditionalSaveData(seeded);
|
||||
book.tick();
|
||||
CompoundTag saved = new CompoundTag();
|
||||
book.saveWithoutId(saved);
|
||||
helper.assertValueEqual(saved.getShort("Age"), (short)-32_768,
|
||||
"The dropped book must not expire after vanilla's five minutes");
|
||||
helper.assertValueEqual(saved.getShort("Health"), (short)4,
|
||||
"A dropped book must lose one item-health point after 10,000 loaded ticks");
|
||||
helper.assertValueEqual(saved.getInt("BookDecayTicks"), 0,
|
||||
"Slow decay must reset its interval and persist across a restart");
|
||||
DroppedBookEntity restored = new DroppedBookEntity(ModEntities.DROPPED_BOOK.get(), helper.getLevel());
|
||||
restored.readAdditionalSaveData(saved);
|
||||
CompoundTag resaved = new CompoundTag();
|
||||
restored.saveWithoutId(resaved);
|
||||
helper.assertTrue(restored.getItem().is(ModItems.LINKING_BOOK.get()),
|
||||
"The custom book entity must retain its book on reload");
|
||||
helper.assertValueEqual(resaved.getShort("Age"), (short)-32_768,
|
||||
"The custom book must remain non-despawning after reload");
|
||||
helper.assertValueEqual(resaved.getShort("Health"), (short)4,
|
||||
"Decay damage must persist after reload");
|
||||
helper.assertTrue(restored.copy().getItem().is(ModItems.LINKING_BOOK.get()),
|
||||
"A pickup-animation copy must retain the book data after reload");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -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,11 +11,15 @@ import net.minecraft.network.chat.Component;
|
||||
/** Flags written into a Link Panel and carried by books assembled from it. */
|
||||
public record LinkProperties(int flags) {
|
||||
public static final int DISARM = 1;
|
||||
/** Retained only to read pre-0.4.7.1 test books; no longer generated or applied. */
|
||||
/** Linking Books do not create a platform unless this Legacy flag is enabled. */
|
||||
public static final int GENERATE_PLATFORM = 2;
|
||||
public static final int MAINTAIN_MOMENTUM = 4;
|
||||
public static final int INTRA_LINKING = 8;
|
||||
private static final int ALL_FLAGS = DISARM | GENERATE_PLATFORM | MAINTAIN_MOMENTUM | INTRA_LINKING;
|
||||
public static final int INTRA_LINKING_ONLY = 16;
|
||||
public static final int RELATIVE_LINK = 32;
|
||||
public static final int DISABLE_PLATFORM = 64;
|
||||
private static final int ALL_FLAGS = DISARM | GENERATE_PLATFORM | MAINTAIN_MOMENTUM | INTRA_LINKING
|
||||
| INTRA_LINKING_ONLY | RELATIVE_LINK | DISABLE_PLATFORM;
|
||||
public static final LinkProperties NONE = new LinkProperties(0);
|
||||
public static final Codec<LinkProperties> CODEC = Codec.intRange(0, ALL_FLAGS).xmap(
|
||||
LinkProperties::new, LinkProperties::flags);
|
||||
@@ -35,13 +39,17 @@ public record LinkProperties(int flags) {
|
||||
|
||||
/** Intra Linking only governs Linking Books; a Descriptive Book treats that panel as ordinary. */
|
||||
public LinkProperties forDescriptiveBook() {
|
||||
return new LinkProperties(flags & ~INTRA_LINKING);
|
||||
return new LinkProperties(flags & ~(INTRA_LINKING | INTRA_LINKING_ONLY));
|
||||
}
|
||||
|
||||
public void appendTooltip(List<Component> lines) {
|
||||
if (has(DISARM)) lines.add(propertyName("disarm"));
|
||||
if (has(MAINTAIN_MOMENTUM)) lines.add(propertyName("maintain_momentum"));
|
||||
if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking"));
|
||||
if (has(INTRA_LINKING_ONLY)) lines.add(propertyName("intra_linking_only"));
|
||||
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"));
|
||||
}
|
||||
|
||||
private static Component propertyName(String name) {
|
||||
@@ -54,6 +62,10 @@ public record LinkProperties(int flags) {
|
||||
case DISARM -> propertyName("disarm");
|
||||
case MAINTAIN_MOMENTUM -> propertyName("maintain_momentum");
|
||||
case INTRA_LINKING -> propertyName("intra_linking");
|
||||
case INTRA_LINKING_ONLY -> propertyName("intra_linking_only");
|
||||
case RELATIVE_LINK -> propertyName("relative_link");
|
||||
case GENERATE_PLATFORM -> propertyName("generate_platform");
|
||||
case DISABLE_PLATFORM -> propertyName("disable_platform");
|
||||
default -> throw new IllegalArgumentException("Unknown link property flag: " + flag);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,14 +5,21 @@ import java.util.Optional;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
||||
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.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.border.WorldBorder;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.level.portal.DimensionTransition;
|
||||
import net.neoforged.fml.ModList;
|
||||
|
||||
/** Server-side validation and execution for book travel. */
|
||||
@@ -32,6 +39,36 @@ public final class LinkingService {
|
||||
return travel(player, target, false, properties, false);
|
||||
}
|
||||
|
||||
/** Removes only the active handheld book after a successful link, leaving it at the departure point. */
|
||||
public static LinkResult travelHandheld(ServerPlayer player, InteractionHand hand, ItemStack activeBook,
|
||||
LinkTarget target, LinkProperties properties, boolean requiresFooting) {
|
||||
if (player.getItemInHand(hand) != activeBook || activeBook.isEmpty()) {
|
||||
return LinkResult.NO_SAFE_DESTINATION;
|
||||
}
|
||||
|
||||
ServerLevel sourceLevel = player.serverLevel();
|
||||
Vec3 sourcePosition = player.position();
|
||||
player.setItemInHand(hand, ItemStack.EMPTY);
|
||||
boolean dropped = false;
|
||||
try {
|
||||
LinkResult result = requiresFooting
|
||||
? travelWithFooting(player, target, properties)
|
||||
: travel(player, target, properties);
|
||||
if (result != LinkResult.SUCCESS) return result;
|
||||
|
||||
DroppedBookEntity book = new DroppedBookEntity(sourceLevel, sourcePosition, activeBook);
|
||||
dropped = sourceLevel.addFreshEntity(book);
|
||||
return result;
|
||||
} finally {
|
||||
// Failed validation or a rejected entity spawn must never eat the book.
|
||||
if (!dropped) {
|
||||
if (player.getItemInHand(hand).isEmpty()) player.setItemInHand(hand, activeBook);
|
||||
else if (!player.getInventory().add(activeBook)) player.drop(activeBook, false);
|
||||
}
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/** Portal travel uses the vanilla transfer if its destination is not yet loaded. */
|
||||
public static LinkResult travelPortal(ServerPlayer player, LinkTarget target, LinkProperties properties) {
|
||||
return travel(player, target, false, properties, true);
|
||||
@@ -53,16 +90,19 @@ public final class LinkingService {
|
||||
if (destinationLevel == null) {
|
||||
return LinkResult.MISSING_DIMENSION;
|
||||
}
|
||||
Optional<Vec3> destination = resolveDestination(destinationLevel, target.location().pos(), true);
|
||||
if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel())
|
||||
return LinkResult.INTRA_ONLY;
|
||||
BlockPos origin = landingOrigin(player, destinationLevel, target, properties);
|
||||
Optional<Vec3> destination = resolveDestination(destinationLevel, origin, true);
|
||||
if (destination.isEmpty()) {
|
||||
return LinkResult.NO_SAFE_DESTINATION;
|
||||
}
|
||||
|
||||
Vec3 position = destination.get();
|
||||
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);
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
|
||||
applyMomentum(player, momentum, target, properties);
|
||||
player.resetFallDistance();
|
||||
return LinkResult.SUCCESS;
|
||||
}
|
||||
@@ -72,12 +112,15 @@ public final class LinkingService {
|
||||
ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension());
|
||||
if (destinationLevel == null) return LinkResult.MISSING_DIMENSION;
|
||||
boolean instant = destinationChunksLoaded(destinationLevel, target.location().pos());
|
||||
Optional<Vec3> destination = resolveDestination(destinationLevel, target.location().pos(), true);
|
||||
if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel())
|
||||
return LinkResult.INTRA_ONLY;
|
||||
Optional<Vec3> destination = resolveDestination(destinationLevel,
|
||||
landingOrigin(player, destinationLevel, target, properties), true);
|
||||
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
|
||||
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);
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
|
||||
applyMomentum(player, momentum, target, properties);
|
||||
player.resetFallDistance();
|
||||
return LinkResult.SUCCESS;
|
||||
}
|
||||
@@ -88,34 +131,93 @@ public final class LinkingService {
|
||||
if (destinationLevel == null) {
|
||||
return LinkResult.MISSING_DIMENSION;
|
||||
}
|
||||
if (destinationLevel == player.level() && !properties.has(LinkProperties.INTRA_LINKING))
|
||||
if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel())
|
||||
return LinkResult.INTRA_ONLY;
|
||||
if (destinationLevel == player.level() && !properties.has(LinkProperties.INTRA_LINKING)
|
||||
&& !properties.has(LinkProperties.INTRA_LINKING_ONLY))
|
||||
return LinkResult.INTRA_LINKING_REQUIRED;
|
||||
|
||||
boolean instant = !portal || destinationChunksLoaded(destinationLevel, target.location().pos());
|
||||
Optional<Vec3> destination = resolveDestination(destinationLevel, target.location().pos(), requiresFooting);
|
||||
Optional<Vec3> destination = resolveDestination(destinationLevel,
|
||||
landingOrigin(player, destinationLevel, target, properties), requiresFooting);
|
||||
if (destination.isEmpty()) {
|
||||
return LinkResult.NO_SAFE_DESTINATION;
|
||||
}
|
||||
|
||||
Vec3 position = destination.get();
|
||||
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);
|
||||
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();
|
||||
return LinkResult.SUCCESS;
|
||||
}
|
||||
|
||||
/** 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();
|
||||
/** 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. */
|
||||
private static BlockPos landingOrigin(Entity player, ServerLevel destinationLevel,
|
||||
LinkTarget target, LinkProperties properties) {
|
||||
if (!properties.has(LinkProperties.RELATIVE_LINK)) return target.location().pos();
|
||||
BlockPos sourceSpawn = ((ServerLevel) player.level()).getSharedSpawnPos();
|
||||
BlockPos destinationSpawn = destinationLevel.getSharedSpawnPos();
|
||||
BlockPos departure = player.blockPosition();
|
||||
return destinationSpawn.offset(departure.getX() - sourceSpawn.getX(),
|
||||
departure.getY() - sourceSpawn.getY(), departure.getZ() - sourceSpawn.getZ());
|
||||
}
|
||||
|
||||
private static void teleport(
|
||||
@@ -230,6 +332,8 @@ public final class LinkingService {
|
||||
SUCCESS,
|
||||
MISSING_DIMENSION,
|
||||
INTRA_LINKING_REQUIRED,
|
||||
INTRA_ONLY,
|
||||
DISARM_REJECTED,
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.registry.ModEntities;
|
||||
import net.minecraft.client.renderer.item.ItemProperties;
|
||||
import net.minecraft.client.renderer.ItemBlockRenderTypes;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
@@ -25,6 +26,7 @@ import net.dnorberg.rivencraft.client.screen.NotebookScreen;
|
||||
import net.dnorberg.rivencraft.client.screen.WritingDeskScreen;
|
||||
import net.dnorberg.rivencraft.client.screen.BookBinderScreen;
|
||||
import net.dnorberg.rivencraft.client.screen.InkMixerScreen;
|
||||
import net.dnorberg.rivencraft.client.screen.LinkModifierScreen;
|
||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
@@ -34,7 +36,9 @@ public final class ClientEvents {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
|
||||
event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::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.STAR_FISSURE.get(), StarFissureRenderer::new);
|
||||
}
|
||||
@@ -45,6 +49,7 @@ public final class ClientEvents {
|
||||
event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new);
|
||||
event.register(ModMenus.BOOK_BINDER.get(), BookBinderScreen::new);
|
||||
event.register(ModMenus.INK_MIXER.get(), InkMixerScreen::new);
|
||||
event.register(ModMenus.LINK_MODIFIER.get(), LinkModifierScreen::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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.entity.DroppedBookEntity;
|
||||
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.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** Renders the same open book model and textures as Rivencraft's book stands. */
|
||||
public final class DroppedBookRenderer extends EntityRenderer<DroppedBookEntity> {
|
||||
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 DroppedBookRenderer(EntityRendererProvider.Context context) {
|
||||
super(context);
|
||||
model = new BookModel(context.bakeLayer(ModelLayers.BOOK));
|
||||
shadowRadius = 0.25F;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DroppedBookEntity entity, float yaw, float partialTick, PoseStack poseStack,
|
||||
MultiBufferSource buffers, int packedLight) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.0F, 0.0625F, 0.0F);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(yaw + 90.0F));
|
||||
// The opened model's covers rotate toward the YZ plane. As in Legacy's
|
||||
// dropped-book renderer, a Z-axis quarter-turn lays the pages skyward.
|
||||
poseStack.mulPose(Axis.ZP.rotationDegrees(90.0F));
|
||||
poseStack.scale(0.8F, 0.8F, 0.8F);
|
||||
model.setupAnim(0.0F, 0.0F, 0.0F, 1.2F);
|
||||
VertexConsumer vertices = buffers.getBuffer(RenderType.entitySolid(getTextureLocation(entity)));
|
||||
model.render(poseStack, vertices, packedLight, net.minecraft.client.renderer.texture.OverlayTexture.NO_OVERLAY, -1);
|
||||
poseStack.popPose();
|
||||
super.render(entity, yaw, partialTick, poseStack, buffers, packedLight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation(DroppedBookEntity entity) {
|
||||
return entity.getItem().is(ModItems.DESCRIPTIVE_BOOK.get()) ? DESCRIPTIVE_TEXTURE : LINKING_TEXTURE;
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ public final class SymbolPageTextures {
|
||||
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) {
|
||||
int[] digits = SymbolNumerals.digitsFor(number);
|
||||
int digitSize = digits.length == 1 ? size : Math.max(8, size * 3 / 4);
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
@@ -48,6 +49,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
private static final Component EXPLORE_ICON = Component.literal("👁");
|
||||
|
||||
private final AgeReference age;
|
||||
private final Component displayName;
|
||||
private final ResourceLocation generationProfile;
|
||||
private final AgeTraits traits;
|
||||
private final OrderedWriting writing;
|
||||
@@ -68,6 +70,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
List<SymbolDisplayMetadata> writingSymbols) {
|
||||
super(book.getHoverName());
|
||||
age = book.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
displayName = book.get(DataComponents.CUSTOM_NAME);
|
||||
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
|
||||
this.traits = traits;
|
||||
writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY);
|
||||
@@ -82,6 +85,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
List<SymbolDisplayMetadata> writingSymbols) {
|
||||
super(book.getHoverName());
|
||||
age = book.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
displayName = book.get(DataComponents.CUSTOM_NAME);
|
||||
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
|
||||
this.traits = traits;
|
||||
writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY);
|
||||
@@ -245,7 +249,8 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
}
|
||||
return;
|
||||
}
|
||||
int nameEnd = drawWrappedWithoutShadow(graphics, Component.literal(age.name()),
|
||||
int nameEnd = drawWrappedWithoutShadow(graphics,
|
||||
displayName == null ? Component.literal(age.name()) : displayName,
|
||||
x, top + 40, 110);
|
||||
int detailsY = drawWrappedWithoutShadow(graphics,
|
||||
author == null || author.isBlank()
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package net.dnorberg.rivencraft.client.screen;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
|
||||
import net.dnorberg.rivencraft.network.SetLinkModifierTitlePayload;
|
||||
import net.dnorberg.rivencraft.network.SetLinkModifierSeedPayload;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
|
||||
/** Legacy single-slot layout, with server-owned link options. */
|
||||
public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifierMenu> {
|
||||
private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "textures/gui/link_modifier_legacy.png");
|
||||
private static final int[] FLAGS = {LinkProperties.DISARM, LinkProperties.INTRA_LINKING,
|
||||
LinkProperties.INTRA_LINKING_ONLY, LinkProperties.MAINTAIN_MOMENTUM,
|
||||
LinkProperties.RELATIVE_LINK, LinkProperties.DISABLE_PLATFORM};
|
||||
private static final int[] IDS = {LinkModifierMenu.TOGGLE_DISARM, LinkModifierMenu.TOGGLE_INTRA,
|
||||
LinkModifierMenu.TOGGLE_INTRA_ONLY, LinkModifierMenu.TOGGLE_MOMENTUM,
|
||||
LinkModifierMenu.TOGGLE_RELATIVE, LinkModifierMenu.TOGGLE_PLATFORM};
|
||||
private EditBox titleInput;
|
||||
private EditBox seedInput;
|
||||
private ItemStack lastBook = ItemStack.EMPTY;
|
||||
private long lastSeed = Long.MIN_VALUE;
|
||||
|
||||
public LinkModifierScreen(LinkModifierMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title);
|
||||
imageWidth = 176;
|
||||
imageHeight = 166;
|
||||
inventoryLabelX = 8;
|
||||
inventoryLabelY = 72;
|
||||
}
|
||||
|
||||
@Override protected void init() {
|
||||
super.init();
|
||||
titleInput = new EditBox(font, leftPos + 80, topPos + 56, 87, 14,
|
||||
Component.translatable("screen.rivencraft.link_modifier.title"));
|
||||
titleInput.setMaxLength(32);
|
||||
titleInput.setResponder(value -> PacketDistributor.sendToServer(
|
||||
new SetLinkModifierTitlePayload(menu.containerId, value)));
|
||||
addRenderableWidget(titleInput);
|
||||
seedInput = new EditBox(font, leftPos + 80, topPos + 15, 87, 14,
|
||||
Component.translatable("screen.rivencraft.link_modifier.seed"));
|
||||
seedInput.setMaxLength(21);
|
||||
seedInput.setTooltip(Tooltip.create(Component.translatable("screen.rivencraft.link_modifier.seed_warning")));
|
||||
addRenderableWidget(seedInput);
|
||||
refreshControls();
|
||||
}
|
||||
|
||||
private void click(int id) {
|
||||
if (minecraft != null && minecraft.gameMode != null)
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id);
|
||||
}
|
||||
|
||||
@Override protected void containerTick() { refreshControls(); }
|
||||
|
||||
private void submitSeed() {
|
||||
if (seedInput.visible && !seedInput.getValue().equals(Long.toString(menu.seed())))
|
||||
PacketDistributor.sendToServer(new SetLinkModifierSeedPayload(menu.containerId, seedInput.getValue()));
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||
if (seedInput.isFocused()) {
|
||||
if (keyCode == 257 || keyCode == 335) submitSeed();
|
||||
else if (keyCode != 256) seedInput.keyPressed(keyCode, scanCode, modifiers);
|
||||
if (keyCode == 256 || keyCode == 257 || keyCode == 335) {
|
||||
setFocused(null);
|
||||
seedInput.setFocused(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (titleInput.isFocused()) {
|
||||
if (keyCode == 256) {
|
||||
setFocused(null);
|
||||
titleInput.setFocused(false);
|
||||
} else titleInput.keyPressed(keyCode, scanCode, modifiers);
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (seedInput.isFocused() && (mouseX < seedInput.getX() || mouseX >= seedInput.getX() + seedInput.getWidth()
|
||||
|| mouseY < seedInput.getY() || mouseY >= seedInput.getY() + seedInput.getHeight())) {
|
||||
submitSeed();
|
||||
setFocused(null);
|
||||
seedInput.setFocused(false);
|
||||
}
|
||||
if (button == 0) {
|
||||
for (int index = 0; index < FLAGS.length; index++) {
|
||||
if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) {
|
||||
if (!menu.book().isEmpty()) click(IDS[index]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (menu.canDelete(minecraft.player)
|
||||
&& inside(mouseX, mouseY, menu.awaitingDeletionConfirmation() ? 140 : 120, 32, 18, 18)) {
|
||||
click(LinkModifierMenu.DELETE_AGE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
private boolean inside(double mouseX, double mouseY, int x, int y, int width, int height) {
|
||||
return mouseX >= leftPos + x && mouseX < leftPos + x + width
|
||||
&& mouseY >= topPos + y && mouseY < topPos + y + height;
|
||||
}
|
||||
|
||||
private void refreshControls() {
|
||||
ItemStack book = menu.book();
|
||||
if (!ItemStack.isSameItemSameComponents(book, lastBook) && !titleInput.isFocused()) {
|
||||
Component name = book.get(DataComponents.CUSTOM_NAME);
|
||||
titleInput.setValue(name == null ? "" : name.getString());
|
||||
lastBook = book.copy();
|
||||
}
|
||||
boolean present = !book.isEmpty();
|
||||
titleInput.setEditable(present);
|
||||
seedInput.visible = menu.hasSeed();
|
||||
seedInput.setEditable(menu.hasSeed());
|
||||
if (seedInput.visible && !seedInput.isFocused() && (lastSeed != menu.seed()
|
||||
|| !ItemStack.isSameItemSameComponents(book, lastBook))) {
|
||||
lastSeed = menu.seed();
|
||||
seedInput.setValue(Long.toString(lastSeed));
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
|
||||
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
LinkProperties properties = menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
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,
|
||||
selected, false);
|
||||
}
|
||||
if (menu.canDelete(minecraft.player)) {
|
||||
square(graphics, 120, 32, menu.awaitingDeletionConfirmation(), false);
|
||||
if (menu.awaitingDeletionConfirmation()) square(graphics, 140, 32, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void square(GuiGraphics graphics, int x, int y, boolean selected, boolean danger) {
|
||||
int left = leftPos + x;
|
||||
int top = topPos + y;
|
||||
graphics.fill(left, top, left + 18, top + 18, 0xFFAAAAAA);
|
||||
graphics.fill(left + 2, top + 2, left + 17, top + 17,
|
||||
danger ? 0xFFAA5555 : selected ? 0xFFBBBBBB : 0xFF777777);
|
||||
}
|
||||
|
||||
@Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
|
||||
graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
|
||||
}
|
||||
|
||||
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.render(graphics, mouseX, mouseY, partialTick);
|
||||
renderTooltip(graphics, mouseX, mouseY);
|
||||
if (!menu.book().isEmpty()) {
|
||||
for (int index = 0; index < FLAGS.length; index++) {
|
||||
if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) {
|
||||
Component label = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
|
||||
? Component.literal("Generate Platform: "
|
||||
+ (platformEnabled(menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(),
|
||||
LinkProperties.NONE)) ? "On" : "Off"))
|
||||
: LinkProperties.name(FLAGS[index]);
|
||||
graphics.renderTooltip(font, label, mouseX, mouseY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (menu.canDelete(minecraft.player)
|
||||
&& inside(mouseX, mouseY, menu.awaitingDeletionConfirmation() ? 140 : 120, 32, 18, 18))
|
||||
graphics.renderTooltip(font, Component.translatable(menu.awaitingDeletionConfirmation()
|
||||
? "screen.rivencraft.link_modifier.confirm" : "screen.rivencraft.link_modifier.retire"),
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -182,9 +182,9 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
||||
|
||||
private int displayedPageSlots() {
|
||||
if (menu.getCarried().is(ModItems.PAGE.get())) {
|
||||
return NotebookMenu.NOTEBOOK_SLOTS;
|
||||
return menu.pageCapacity();
|
||||
}
|
||||
for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) {
|
||||
for (int index = menu.pageCapacity() - 1; index >= 0; index--) {
|
||||
if (!menu.getSlot(index).getItem().isEmpty()) {
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
@@ -150,8 +151,8 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
||||
if (tab == menu.activeCollection() && !allMode) graphics.setColor(0.5F, 0.5F, 1F, 1F);
|
||||
graphics.blit(LEGACY_DESK, x, tabY, 0, 166, 58, 37);
|
||||
graphics.setColor(1F, 1F, 1F, 1F);
|
||||
// Encode the visible one-based tab number with Legacy's numeric word glyph.
|
||||
SymbolPageTextures.drawNumeral(graphics, tab + 1, x + 8, tabY + 3, 19);
|
||||
// Legacy numbers its Writing Desk collections from zero.
|
||||
SymbolPageTextures.drawNumeral(graphics, tab, x + (tab < 25 ? 8 : 4), tabY + 3, 19);
|
||||
ItemStack collection = menu.getSlot(tab).getItem();
|
||||
if (!collection.isEmpty()) {
|
||||
String name = collection.getHoverName().getString();
|
||||
@@ -226,7 +227,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
||||
else if (mouseX < leftPos + 34 && localY >= 29 && localY < 177) {
|
||||
int tab = menu.firstVisibleTab() + (localY - 29) / 37;
|
||||
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.tab_symbol",
|
||||
tab + 1), mouseX, mouseY);
|
||||
tab), mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +235,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
||||
private List<ItemStack> pages(int collectionIndex) {
|
||||
ItemStack collection = menu.getSlot(collectionIndex).getItem();
|
||||
if (collection.is(ModItems.PAGE.get())) return List.of(collection);
|
||||
if (collection.is(ModItems.NOTEBOOK.get())) {
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
}
|
||||
return List.of();
|
||||
@@ -301,9 +302,9 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
||||
|
||||
private boolean canMovePage(DisplayPage source, DisplayPage target) {
|
||||
return !source.stack().isEmpty()
|
||||
&& (menu.getSlot(source.collection()).getItem().is(ModItems.NOTEBOOK.get())
|
||||
&& (CollationFolderItem.isCollection(menu.getSlot(source.collection()).getItem())
|
||||
|| menu.getSlot(source.collection()).getItem().is(ModItems.PAGE.get()))
|
||||
&& (menu.getSlot(target.collection()).getItem().is(ModItems.NOTEBOOK.get())
|
||||
&& (CollationFolderItem.isCollection(menu.getSlot(target.collection()).getItem())
|
||||
|| menu.getSlot(target.collection()).getItem().is(ModItems.PAGE.get()))
|
||||
&& (source.collection() != target.collection() || source.index() != target.index());
|
||||
}
|
||||
@@ -317,10 +318,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
||||
if (!slot.mayPlace(held)) return -1;
|
||||
ItemStack current = slot.getItem();
|
||||
if (current.isEmpty()) return index;
|
||||
if (index < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) {
|
||||
if (index < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) {
|
||||
List<ItemStack> pages = current.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
if (pages.size() < NotebookContents.MAX_PAGES || pages.stream().anyMatch(ItemStack::isEmpty))
|
||||
if (pages.size() < CollationFolderItem.capacity(current) || pages.stream().anyMatch(ItemStack::isEmpty))
|
||||
return index;
|
||||
} else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held)
|
||||
&& current.getCount() < slot.getMaxStackSize(held)) return index;
|
||||
|
||||
@@ -112,8 +112,9 @@ public final class RivencraftCommands {
|
||||
source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false);
|
||||
for (AgeDefinition age : ages) {
|
||||
boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null;
|
||||
boolean retired = AgeRegistrySavedData.get(source.getLevel()).isDead(age.id());
|
||||
source.sendSuccess(() -> Component.literal(
|
||||
(loaded ? "[loaded] " : "[unloaded] ")
|
||||
(retired ? "[retired] " : loaded ? "[loaded] " : "[unloaded] ")
|
||||
+ age.name() + " — " + age.dimensionId()
|
||||
+ (age.seasonMode() == null ? "" : " (season: "
|
||||
+ age.seasonMode().name().toLowerCase(java.util.Locale.ROOT) + ")")
|
||||
|
||||
@@ -25,12 +25,14 @@ import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||
import net.dnorberg.rivencraft.writing.InkModifierIngredients;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -63,6 +65,11 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
||||
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
|
||||
@@ -72,6 +79,7 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
||||
var connection = Minecraft.getInstance().getConnection();
|
||||
if (connection != null) {
|
||||
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
|
||||
pages.addAll(ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess()));
|
||||
} else {
|
||||
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
|
||||
.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.
|
||||
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
|
||||
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();
|
||||
if (connection == null) return;
|
||||
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
|
||||
@@ -98,6 +108,19 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
||||
if (!missing.isEmpty()) {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package net.dnorberg.rivencraft.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModEntities;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** A recoverable, model-rendered book left behind by handheld linking. */
|
||||
public final class DroppedBookEntity extends ItemEntity {
|
||||
private static final String DECAY_TICKS_TAG = "BookDecayTicks";
|
||||
private static final int SLOW_DECAY_INTERVAL = 10_000;
|
||||
private int decayTicks;
|
||||
|
||||
public DroppedBookEntity(EntityType<DroppedBookEntity> type, Level level) {
|
||||
super(type, level);
|
||||
}
|
||||
|
||||
public DroppedBookEntity(Level level, Vec3 position, ItemStack stack) {
|
||||
this(ModEntities.DROPPED_BOOK.get(), level);
|
||||
setPos(position.x, position.y + 0.25, position.z);
|
||||
setDeltaMovement(Vec3.ZERO);
|
||||
setItem(stack);
|
||||
setUnlimitedLifetime();
|
||||
setPickUpDelay(20);
|
||||
}
|
||||
|
||||
/** Vanilla's pickup particle calls ItemEntity.copy() and renders the returned entity. */
|
||||
@Override
|
||||
public DroppedBookEntity copy() {
|
||||
DroppedBookEntity copy = new DroppedBookEntity(ModEntities.DROPPED_BOOK.get(), level());
|
||||
copy.copyPosition(this);
|
||||
copy.setItem(getItem().copy());
|
||||
copy.setUnlimitedLifetime();
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (level().isClientSide || isRemoved()) return;
|
||||
if (++decayTicks >= SLOW_DECAY_INTERVAL) {
|
||||
decayTicks = 0;
|
||||
hurt(damageSources().starve(), 1.0F);
|
||||
}
|
||||
if (!isRemoved() && isInWaterOrRain()) hurt(damageSources().drown(), 1.0F);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAdditionalSaveData(CompoundTag tag) {
|
||||
super.addAdditionalSaveData(tag);
|
||||
tag.putInt(DECAY_TICKS_TAG, decayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readAdditionalSaveData(CompoundTag tag) {
|
||||
super.readAdditionalSaveData(tag);
|
||||
decayTicks = Math.max(0, Math.min(tag.getInt(DECAY_TICKS_TAG), SLOW_DECAY_INTERVAL - 1));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import net.dnorberg.rivencraft.block.CrystalPortalBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
||||
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.CrystalPortalSurfaces;
|
||||
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
|
||||
@@ -19,7 +22,11 @@ import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
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.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
@@ -154,13 +161,54 @@ public final class CrystalPortalGameTests {
|
||||
BlockPos holderPos = helper.absolutePos(new BlockPos(0, 2, 1));
|
||||
level.setBlock(holderPos, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState()
|
||||
.setValue(BookReceptacleBlock.FACING, Direction.UP), 3);
|
||||
((BookReceptacleBlockEntity) level.getBlockEntity(holderPos))
|
||||
.setBook(new ItemStack(ModItems.LINKING_BOOK.get()));
|
||||
LinkTarget destination = new LinkTarget(GlobalPos.of(level.dimension(), centerTarget(helper)), 0, 0);
|
||||
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)),
|
||||
"The horizontal crystal opening must activate");
|
||||
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
|
||||
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
|
||||
"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();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,26 @@ import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
public final class Milestone7PolishGameTests {
|
||||
private Milestone7PolishGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void modifierFlagsKeepLegacyIntraRules(GameTestHelper helper) {
|
||||
LinkProperties linking = new LinkProperties(LinkProperties.DISARM | LinkProperties.INTRA_LINKING
|
||||
| LinkProperties.INTRA_LINKING_ONLY | LinkProperties.RELATIVE_LINK
|
||||
| LinkProperties.DISABLE_PLATFORM);
|
||||
helper.assertTrue(linking.has(LinkProperties.INTRA_LINKING_ONLY)
|
||||
&& linking.has(LinkProperties.RELATIVE_LINK)
|
||||
&& linking.has(LinkProperties.DISABLE_PLATFORM),
|
||||
"The new link options must retain independent flag values");
|
||||
LinkProperties descriptive = linking.forDescriptiveBook();
|
||||
helper.assertTrue(!descriptive.has(LinkProperties.INTRA_LINKING)
|
||||
&& !descriptive.has(LinkProperties.INTRA_LINKING_ONLY),
|
||||
"Intra-only controls must not constrain Descriptive Books");
|
||||
helper.assertTrue(descriptive.has(LinkProperties.DISARM)
|
||||
&& descriptive.has(LinkProperties.RELATIVE_LINK)
|
||||
&& descriptive.has(LinkProperties.DISABLE_PLATFORM),
|
||||
"Shared link options must remain active on Descriptive Books");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void linkSoundPriorityMatchesLegacy(GameTestHelper helper) {
|
||||
helper.assertTrue(LinkVisualEffects.sound(LinkProperties.NONE, false) == ModSounds.LINK.get(),
|
||||
|
||||
@@ -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");
|
||||
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(1), new int[] {56}),
|
||||
"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}),
|
||||
"Twenty-four must combine Legacy's five and unit marks");
|
||||
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(25), new int[] {1, 0}),
|
||||
"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");
|
||||
|
||||
String display = "\"name\":\"symbol.example.test\","
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Legacy's compact page collection, using the existing Notebook page editor. */
|
||||
public final class CollationFolderItem extends NotebookItem {
|
||||
public static final int MAX_PAGES = 27;
|
||||
|
||||
public CollationFolderItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static boolean isCollection(ItemStack stack) {
|
||||
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get())
|
||||
|| stack.is(ModItems.SYMBOL_PORTFOLIO.get());
|
||||
}
|
||||
|
||||
public static int capacity(ItemStack stack) {
|
||||
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES
|
||||
: stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.SYMBOL_PORTFOLIO.get())
|
||||
? NotebookContents.MAX_PAGES : 0;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
@@ -41,14 +42,18 @@ public final class DescriptiveBookItem extends Item {
|
||||
}
|
||||
|
||||
public static boolean useWrittenBook(ServerPlayer player, ItemStack stack) {
|
||||
return useWrittenBook(player, stack, false);
|
||||
return useWrittenBook(player, stack, false, null);
|
||||
}
|
||||
|
||||
public static boolean useHandheldBook(ServerPlayer player, InteractionHand hand) {
|
||||
return useWrittenBook(player, player.getItemInHand(hand), false, hand);
|
||||
}
|
||||
|
||||
public static boolean usePortalBook(ServerPlayer player, ItemStack stack) {
|
||||
return useWrittenBook(player, stack, true);
|
||||
return useWrittenBook(player, stack, true, null);
|
||||
}
|
||||
|
||||
private static boolean useWrittenBook(ServerPlayer player, ItemStack stack, boolean portal) {
|
||||
private static boolean useWrittenBook(ServerPlayer player, ItemStack stack, boolean portal, InteractionHand hand) {
|
||||
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
if (age == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unwritten"), true);
|
||||
@@ -56,7 +61,10 @@ public final class DescriptiveBookItem extends Item {
|
||||
}
|
||||
|
||||
LinkTarget returnTarget = LinkTarget.from(player);
|
||||
LinkTarget ageTarget = AgeDimensionService.resolve(player, age).orElse(null);
|
||||
LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
|
||||
.forDescriptiveBook();
|
||||
LinkTarget ageTarget = AgeDimensionService.resolve(player, age, net.dnorberg.rivencraft.writing.OrderedWriting.EMPTY,
|
||||
!properties.has(LinkProperties.DISABLE_PLATFORM)).orElse(null);
|
||||
if (ageTarget == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
|
||||
return false;
|
||||
@@ -64,11 +72,10 @@ public final class DescriptiveBookItem extends Item {
|
||||
|
||||
ServerLevel sourceLevel = player.serverLevel();
|
||||
Vec3 sourcePosition = player.position();
|
||||
LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
|
||||
.forDescriptiveBook();
|
||||
LinkingService.LinkResult result = portal
|
||||
? LinkingService.travelDescriptivePortal(player, ageTarget, properties)
|
||||
: LinkingService.travelWithFooting(player, ageTarget, properties);
|
||||
: hand == null ? LinkingService.travelWithFooting(player, ageTarget, properties)
|
||||
: LinkingService.travelHandheld(player, hand, stack, ageTarget, properties, true);
|
||||
if (result != LinkingService.LinkResult.SUCCESS) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unsafe"), true);
|
||||
return false;
|
||||
|
||||
@@ -43,14 +43,19 @@ public final class LinkingBookItem extends Item {
|
||||
}
|
||||
|
||||
public static InteractionResultHolder<ItemStack> useBoundBook(ServerPlayer player, ItemStack stack) {
|
||||
return useBoundBook(player, stack, false);
|
||||
return useBoundBook(player, stack, false, null);
|
||||
}
|
||||
|
||||
public static InteractionResultHolder<ItemStack> useHandheldBook(ServerPlayer player, InteractionHand hand) {
|
||||
return useBoundBook(player, player.getItemInHand(hand), false, hand);
|
||||
}
|
||||
|
||||
public static InteractionResultHolder<ItemStack> usePortalBook(ServerPlayer player, ItemStack stack) {
|
||||
return useBoundBook(player, stack, true);
|
||||
return useBoundBook(player, stack, true, null);
|
||||
}
|
||||
|
||||
private static InteractionResultHolder<ItemStack> useBoundBook(ServerPlayer player, ItemStack stack, boolean portal) {
|
||||
private static InteractionResultHolder<ItemStack> useBoundBook(ServerPlayer player, ItemStack stack, boolean portal,
|
||||
InteractionHand hand) {
|
||||
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
|
||||
if (target == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.unbound"), true);
|
||||
@@ -62,7 +67,8 @@ public final class LinkingBookItem extends Item {
|
||||
LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
LinkingService.LinkResult result = portal
|
||||
? LinkingService.travelPortal(player, target, properties)
|
||||
: LinkingService.travel(player, target, properties);
|
||||
: hand == null ? LinkingService.travel(player, target, properties)
|
||||
: LinkingService.travelHandheld(player, hand, stack, target, properties, false);
|
||||
if (result == LinkingService.LinkResult.SUCCESS) {
|
||||
LinkVisualEffects.play(sourceLevel, sourcePosition, player, properties, portal);
|
||||
ProgressionAdvancements.awardAgeTravel(player);
|
||||
@@ -73,6 +79,7 @@ public final class LinkingBookItem extends Item {
|
||||
String errorKey = switch (result) {
|
||||
case MISSING_DIMENSION -> "message.rivencraft.linking_book.missing_dimension";
|
||||
case INTRA_LINKING_REQUIRED -> "message.rivencraft.linking_book.intra_required";
|
||||
case INTRA_ONLY -> "message.rivencraft.linking_book.intra_only";
|
||||
default -> "message.rivencraft.linking_book.unsafe";
|
||||
};
|
||||
player.displayClientMessage(Component.translatable(errorKey), true);
|
||||
|
||||
@@ -16,7 +16,7 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class NotebookItem extends Item {
|
||||
public class NotebookItem extends Item {
|
||||
public NotebookItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
@@ -39,7 +39,8 @@ public final class NotebookItem extends Item {
|
||||
NotebookContents contents = stack.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
|
||||
tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
|
||||
contents.pageCount(), NotebookContents.MAX_PAGES).withStyle(ChatFormatting.GRAY));
|
||||
contents.pageCount(), CollationFolderItem.capacity(stack))
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
if (contents.unresolvedCount() > 0) {
|
||||
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
|
||||
contents.unresolvedCount()).withStyle(ChatFormatting.RED));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResultHolder;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Opens into a 16-page Collation Folder, as Legacy's booster item did. */
|
||||
public final class SealedNotebookItem extends Item {
|
||||
private static final int PAGE_COUNT = 16;
|
||||
|
||||
public SealedNotebookItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||
ItemStack sealed = player.getItemInHand(hand);
|
||||
if (level.isClientSide) return InteractionResultHolder.sidedSuccess(sealed, true);
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return InteractionResultHolder.pass(sealed);
|
||||
ItemStack folder = open(serverPlayer);
|
||||
if (folder.isEmpty()) return InteractionResultHolder.fail(sealed);
|
||||
if (!player.hasInfiniteMaterials()) sealed.shrink(1);
|
||||
if (sealed.isEmpty()) {
|
||||
player.setItemInHand(hand, folder);
|
||||
} else if (!player.getInventory().add(folder)) {
|
||||
player.drop(folder, false);
|
||||
}
|
||||
return InteractionResultHolder.success(player.getItemInHand(hand));
|
||||
}
|
||||
|
||||
static ItemStack open(ServerPlayer player) {
|
||||
// Current symbol definitions have no Legacy rank. Draw from the authored
|
||||
// symbol pool, excluding virtual block/biome pages and corrupted pages.
|
||||
List<ResourceLocation> candidates = SymbolCatalog.symbols().entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getNamespace().equals("rivencraft")
|
||||
&& !entry.getKey().getPath().startsWith("block/")
|
||||
&& !entry.getKey().getPath().startsWith("biome/")
|
||||
&& !entry.getValue().category().getPath().equals("corrupted"))
|
||||
.map(java.util.Map.Entry::getKey).sorted().toList();
|
||||
if (candidates.isEmpty()) return ItemStack.EMPTY;
|
||||
List<ItemStack> pages = new ArrayList<>(PAGE_COUNT);
|
||||
for (int i = 0; i < PAGE_COUNT; i++) {
|
||||
ResourceLocation id = candidates.get(player.getRandom().nextInt(candidates.size()));
|
||||
pages.add(PageItem.createSymbolPage(new SymbolReference(id)));
|
||||
}
|
||||
ItemStack folder = new ItemStack(ModItems.COLLATION_FOLDER.get());
|
||||
folder.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages));
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
@@ -172,7 +173,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
|
||||
private boolean clickStrip(int index) {
|
||||
ItemStack carried = getCarried();
|
||||
if (carried.is(ModItems.NOTEBOOK.get())) {
|
||||
if (CollationFolderItem.isCollection(carried)) {
|
||||
ItemStack emptied = binder.importNotebookFromStack(carried, Math.min(index == 0 ? 0 : index - 1,
|
||||
binder.nextPageIndex()));
|
||||
if (emptied.isEmpty()) return false;
|
||||
@@ -227,7 +228,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
moved = moveItemStackTo(source, PLAYER_START, slots.size(), true);
|
||||
} else if (source.is(Items.LEATHER) || BookBinderBlockEntity.validCover(source)) {
|
||||
moved = moveItemStackTo(source, BookBinderBlockEntity.COVER, BookBinderBlockEntity.COVER + 1, false);
|
||||
} else if (source.is(ModItems.NOTEBOOK.get())) {
|
||||
} else if (CollationFolderItem.isCollection(source)) {
|
||||
ItemStack emptied = binder.importNotebookFromStack(source, binder.nextPageIndex());
|
||||
if (emptied.isEmpty()) return ItemStack.EMPTY;
|
||||
sourceSlot.setByPlayer(emptied);
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeDeletionService;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.DataSlot;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Server-owned book edits and a two-stage, OP-only Age retirement control. */
|
||||
public final class LinkModifierMenu extends AbstractContainerMenu {
|
||||
public static final int TOGGLE_DISARM = 1;
|
||||
public static final int TOGGLE_MOMENTUM = 2;
|
||||
public static final int TOGGLE_INTRA = 3;
|
||||
public static final int DELETE_AGE = 4;
|
||||
public static final int TOGGLE_INTRA_ONLY = 5;
|
||||
public static final int TOGGLE_RELATIVE = 6;
|
||||
public static final int TOGGLE_PLATFORM = 7;
|
||||
private static final int PLAYER_START = 1;
|
||||
private static final int MAX_TITLE_LENGTH = 32;
|
||||
|
||||
private final LinkModifierBlockEntity modifier;
|
||||
private final Container bookSlot;
|
||||
private final boolean clientSide;
|
||||
private final Player owner;
|
||||
private final int[] seedWords = new int[4];
|
||||
private int confirmation;
|
||||
private UUID pendingAge;
|
||||
|
||||
public LinkModifierMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
instanceof LinkModifierBlockEntity found ? found : null);
|
||||
}
|
||||
|
||||
public LinkModifierMenu(int id, Inventory inventory, LinkModifierBlockEntity modifier) {
|
||||
super(ModMenus.LINK_MODIFIER.get(), id);
|
||||
this.modifier = modifier;
|
||||
this.bookSlot = modifier == null ? new SimpleContainer(1) : modifier;
|
||||
this.clientSide = inventory.player.level().isClientSide;
|
||||
this.owner = inventory.player;
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() { return confirmation; }
|
||||
@Override public void set(int value) { confirmation = value; }
|
||||
});
|
||||
for (int index = 0; index < seedWords.length; index++) {
|
||||
final int word = index;
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide ? seedWords[word] : (int) ((seed() >>> (word * 16)) & 0xffffL);
|
||||
}
|
||||
@Override public void set(int value) { seedWords[word] = value & 0xffff; }
|
||||
});
|
||||
}
|
||||
addSlot(new Slot(bookSlot, 0, 80, 35) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return bookSlot.canPlaceItem(0, stack); }
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
@Override public void setChanged() {
|
||||
super.setChanged();
|
||||
pendingAge = null;
|
||||
confirmation = 0;
|
||||
}
|
||||
});
|
||||
for (int row = 0; row < 3; row++) for (int col = 0; col < 9; col++)
|
||||
addSlot(new Slot(inventory, col + row * 9 + 9, 8 + col * 18, 84 + row * 18));
|
||||
for (int col = 0; col < 9; col++)
|
||||
addSlot(new Slot(inventory, col, 8 + col * 18, 142));
|
||||
}
|
||||
|
||||
public ItemStack book() { return bookSlot.getItem(0); }
|
||||
public boolean hasSeed() {
|
||||
return book().is(ModItems.DESCRIPTIVE_BOOK.get())
|
||||
&& book().has(ModDataComponents.AGE_REFERENCE.get());
|
||||
}
|
||||
public long seed() {
|
||||
if (clientSide) {
|
||||
long result = 0;
|
||||
for (int index = 0; index < seedWords.length; index++)
|
||||
result |= (seedWords[index] & 0xffffL) << (index * 16);
|
||||
return result;
|
||||
}
|
||||
if (!hasSeed()) return 0;
|
||||
AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get());
|
||||
return AgeRegistrySavedData.get(((ServerPlayer) owner).serverLevel()).find(reference.id())
|
||||
.map(AgeDefinition::seed).orElse(0L);
|
||||
}
|
||||
|
||||
public boolean setSeed(Player player, String text) {
|
||||
if (clientSide || !stillValid(player) || !hasSeed() || text == null || text.length() > 21
|
||||
|| !(player instanceof ServerPlayer serverPlayer)) return false;
|
||||
long requested;
|
||||
try { requested = Long.parseLong(text.strip()); }
|
||||
catch (NumberFormatException exception) { return false; }
|
||||
AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get());
|
||||
if (reference == null) return false;
|
||||
boolean changed = AgeRegistrySavedData.get(serverPlayer.serverLevel())
|
||||
.reseed(serverPlayer.getServer(), reference.id(), requested);
|
||||
player.displayClientMessage(Component.translatable(changed
|
||||
? "message.rivencraft.link_modifier.seed_changed"
|
||||
: "message.rivencraft.link_modifier.seed_failed"), false);
|
||||
if (changed) broadcastChanges();
|
||||
return changed;
|
||||
}
|
||||
public boolean awaitingDeletionConfirmation() { return confirmation == 1; }
|
||||
public boolean canDelete(Player player) {
|
||||
return player.hasPermissions(2) && book().is(ModItems.DESCRIPTIVE_BOOK.get())
|
||||
&& book().has(ModDataComponents.AGE_REFERENCE.get());
|
||||
}
|
||||
|
||||
public boolean setTitle(Player player, String title) {
|
||||
if (clientSide || !stillValid(player) || title == null || title.length() > MAX_TITLE_LENGTH
|
||||
|| book().isEmpty()) return false;
|
||||
String clean = title.strip();
|
||||
if (clean.isEmpty()) book().remove(DataComponents.CUSTOM_NAME);
|
||||
else book().set(DataComponents.CUSTOM_NAME, Component.literal(clean));
|
||||
bookSlot.setChanged();
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public boolean clickMenuButton(Player player, int id) {
|
||||
if (clientSide || !stillValid(player) || book().isEmpty()) return false;
|
||||
if (id == DELETE_AGE) return retireAge(player);
|
||||
int flag = switch (id) {
|
||||
case TOGGLE_DISARM -> LinkProperties.DISARM;
|
||||
case TOGGLE_MOMENTUM -> LinkProperties.MAINTAIN_MOMENTUM;
|
||||
case TOGGLE_INTRA -> LinkProperties.INTRA_LINKING;
|
||||
case TOGGLE_INTRA_ONLY -> LinkProperties.INTRA_LINKING_ONLY;
|
||||
case TOGGLE_RELATIVE -> LinkProperties.RELATIVE_LINK;
|
||||
case TOGGLE_PLATFORM -> book().is(ModItems.LINKING_BOOK.get())
|
||||
? LinkProperties.GENERATE_PLATFORM : LinkProperties.DISABLE_PLATFORM;
|
||||
default -> 0;
|
||||
};
|
||||
if (flag == 0 || ((flag == LinkProperties.INTRA_LINKING || flag == LinkProperties.INTRA_LINKING_ONLY)
|
||||
&& book().is(ModItems.DESCRIPTIVE_BOOK.get()))) return false;
|
||||
int flags = book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE).flags();
|
||||
if (flag == LinkProperties.INTRA_LINKING_ONLY && (flags & LinkProperties.INTRA_LINKING_ONLY) == 0)
|
||||
flags |= LinkProperties.INTRA_LINKING;
|
||||
if (flag == LinkProperties.INTRA_LINKING && (flags & LinkProperties.INTRA_LINKING) != 0)
|
||||
flags &= ~LinkProperties.INTRA_LINKING_ONLY;
|
||||
book().set(ModDataComponents.LINK_PROPERTIES.get(), new LinkProperties(flags ^ flag));
|
||||
bookSlot.setChanged();
|
||||
pendingAge = null;
|
||||
confirmation = 0;
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean retireAge(Player player) {
|
||||
if (!(player instanceof ServerPlayer serverPlayer) || !canDelete(player)) return false;
|
||||
AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get());
|
||||
if (reference == null) return false;
|
||||
AgeDefinition age = AgeRegistrySavedData.get(serverPlayer.serverLevel()).find(reference.id()).orElse(null);
|
||||
if (age == null) return false;
|
||||
if (confirmation != 1 || !reference.id().equals(pendingAge)) {
|
||||
pendingAge = reference.id();
|
||||
confirmation = 1;
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
confirmation = 0;
|
||||
pendingAge = null;
|
||||
AgeDeletionService.Result result = AgeDeletionService.retire(serverPlayer.getServer(), age);
|
||||
player.displayClientMessage(Component.translatable(
|
||||
"message.rivencraft.link_modifier.delete." + result.name().toLowerCase()), false);
|
||||
broadcastChanges();
|
||||
return result == AgeDeletionService.Result.RETIRED;
|
||||
}
|
||||
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return modifier != null && modifier.stillValid(player);
|
||||
}
|
||||
|
||||
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
ItemStack source = slot.getItem();
|
||||
ItemStack copy = source.copy();
|
||||
if (index == 0) {
|
||||
if (!moveItemStackTo(source, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY;
|
||||
} else if (!moveItemStackTo(source, 0, 1, false)) return ItemStack.EMPTY;
|
||||
if (source.isEmpty()) slot.set(ItemStack.EMPTY);
|
||||
else slot.setChanged();
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModMenus;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.Container;
|
||||
@@ -28,6 +29,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
private final InteractionHand hand;
|
||||
private final int openedHotbarSlot;
|
||||
private final NotebookContainer contents;
|
||||
private final int pageCapacity;
|
||||
|
||||
public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(containerId, inventory, data.readBoolean() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND);
|
||||
@@ -38,11 +40,12 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
this.playerInventory = inventory;
|
||||
this.hand = hand;
|
||||
this.openedHotbarSlot = inventory.selected;
|
||||
this.pageCapacity = CollationFolderItem.capacity(inventory.player.getItemInHand(hand));
|
||||
this.contents = new NotebookContainer(inventory, hand);
|
||||
|
||||
for (int page = 0; page < NOTEBOOK_SLOTS; page++) {
|
||||
// The client draws these pages on a filtered, scrollable surface instead.
|
||||
addSlot(new PageSlot(contents, page, -1000, -1000));
|
||||
addSlot(new PageSlot(contents, page, -1000, -1000, page < pageCapacity));
|
||||
}
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 9; column++) {
|
||||
@@ -61,11 +64,13 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
: new Slot(playerInventory, inventoryIndex, x, y);
|
||||
}
|
||||
|
||||
public int pageCapacity() { return pageCapacity; }
|
||||
|
||||
@Override
|
||||
public boolean stillValid(Player player) {
|
||||
return player.isAlive()
|
||||
&& (hand == InteractionHand.OFF_HAND || player.getInventory().selected == openedHotbarSlot)
|
||||
&& player.getItemInHand(hand).is(ModItems.NOTEBOOK.get());
|
||||
&& CollationFolderItem.isCollection(player.getItemInHand(hand));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,7 +113,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (source < 0 || source >= NOTEBOOK_SLOTS || target < 0 || target >= NOTEBOOK_SLOTS
|
||||
if (source < 0 || source >= pageCapacity || target < 0 || target >= pageCapacity
|
||||
|| contents.getItem(source).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -132,14 +137,18 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
}
|
||||
|
||||
private static final class PageSlot extends Slot {
|
||||
private PageSlot(Container container, int slot, int x, int y) {
|
||||
private final boolean enabled;
|
||||
|
||||
private PageSlot(Container container, int slot, int x, int y, boolean enabled) {
|
||||
super(container, slot, x, y);
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override public boolean mayPlace(ItemStack stack) {
|
||||
return stack.is(ModItems.PAGE.get())
|
||||
return enabled && stack.is(ModItems.PAGE.get())
|
||||
&& PageItem.content(stack).kind() != PageContent.Kind.LINK_PANEL;
|
||||
}
|
||||
@Override public boolean mayPickup(Player player) { return enabled; }
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
}
|
||||
|
||||
@@ -184,11 +193,12 @@ public final class NotebookMenu extends AbstractContainerMenu {
|
||||
public void setChanged() {
|
||||
super.setChanged();
|
||||
ItemStack notebook = inventory.player.getItemInHand(hand);
|
||||
if (loading || !notebook.is(ModItems.NOTEBOOK.get())) {
|
||||
if (loading || !CollationFolderItem.isCollection(notebook)) {
|
||||
return;
|
||||
}
|
||||
NonNullList<ItemStack> saved = NonNullList.withSize(NOTEBOOK_SLOTS, ItemStack.EMPTY);
|
||||
for (int index = 0; index < NOTEBOOK_SLOTS; index++) {
|
||||
int capacity = CollationFolderItem.capacity(notebook);
|
||||
NonNullList<ItemStack> saved = NonNullList.withSize(capacity, ItemStack.EMPTY);
|
||||
for (int index = 0; index < capacity; index++) {
|
||||
ItemStack page = getItem(index);
|
||||
saved.set(index, page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
@@ -107,7 +108,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
public int pageCapacity(int collectionIndex) {
|
||||
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0;
|
||||
ItemStack collection = desk.getItem(collectionIndex);
|
||||
return collection.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES
|
||||
return CollationFolderItem.isCollection(collection) ? CollationFolderItem.capacity(collection)
|
||||
: collection.is(ModItems.PAGE.get()) ? 1 : 0;
|
||||
}
|
||||
public List<SymbolDisplayMetadata> catalog() { return catalog; }
|
||||
@@ -183,7 +184,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
ItemStack carried = getCarried();
|
||||
if (clientSide || !storablePage(carried)) return false;
|
||||
ItemStack collection = desk.getItem(collectionIndex);
|
||||
if (collection.is(ModItems.NOTEBOOK.get())) {
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
if (pageIndex < 0 || pageIndex >= CollationFolderItem.capacity(collection)) return false;
|
||||
var pages = new java.util.ArrayList<>(collection.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
|
||||
while (pages.size() <= pageIndex) pages.add(ItemStack.EMPTY);
|
||||
@@ -207,8 +209,10 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
if (!getCarried().isEmpty() || sourceCollection == targetCollection && sourcePage == targetPage) return false;
|
||||
ItemStack sourceBook = desk.getItem(sourceCollection);
|
||||
ItemStack targetBook = desk.getItem(targetCollection);
|
||||
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get());
|
||||
boolean targetNotebook = targetBook.is(ModItems.NOTEBOOK.get());
|
||||
boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
|
||||
boolean targetNotebook = CollationFolderItem.isCollection(targetBook);
|
||||
if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)
|
||||
|| targetNotebook && targetPage >= CollationFolderItem.capacity(targetBook)) return false;
|
||||
if ((!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0))
|
||||
|| (!targetNotebook && (!targetBook.is(ModItems.PAGE.get()) || targetPage != 0))) return false;
|
||||
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
|
||||
@@ -247,7 +251,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
private boolean extractPage(int sourceCollection, int sourcePage, int targetSlot) {
|
||||
if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false;
|
||||
ItemStack sourceBook = desk.getItem(sourceCollection);
|
||||
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get());
|
||||
boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
|
||||
if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)) return false;
|
||||
if (!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) return false;
|
||||
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
|
||||
@@ -263,12 +268,12 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
Slot target = slots.get(targetSlot);
|
||||
if (!target.isActive() || !target.mayPlace(moving)) return false;
|
||||
ItemStack current = target.getItem();
|
||||
if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) {
|
||||
if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) {
|
||||
var targetPages = targetSlot == sourceCollection ? sourcePages
|
||||
: new java.util.ArrayList<>(current.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
|
||||
int position = -1;
|
||||
for (int i = 0; i < NotebookContents.MAX_PAGES; i++) {
|
||||
for (int i = 0; i < CollationFolderItem.capacity(current); i++) {
|
||||
if (i >= targetPages.size() || targetPages.get(i).isEmpty()) {
|
||||
position = i;
|
||||
break;
|
||||
@@ -342,7 +347,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
private boolean moveIntoDesk(ItemStack moving) {
|
||||
if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving,
|
||||
WritingDeskBlockEntity.INK, WritingDeskBlockEntity.INK + 1, false);
|
||||
if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
|
||||
if (CollationFolderItem.isCollection(moving)) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
|
||||
if (moving.is(Items.PAPER))
|
||||
return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER,
|
||||
WritingDeskBlockEntity.PAPER + 1, false);
|
||||
@@ -374,7 +379,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
|| pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY;
|
||||
ItemStack collection = desk.getItem(collectionIndex);
|
||||
if (collection.is(ModItems.PAGE.get())) return pageIndex == 0 ? collection : ItemStack.EMPTY;
|
||||
if (collection.is(ModItems.NOTEBOOK.get())) {
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
if (pageIndex >= CollationFolderItem.capacity(collection)) return ItemStack.EMPTY;
|
||||
List<ItemStack> pages = collection.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
return pageIndex < pages.size() ? pages.get(pageIndex) : ItemStack.EMPTY;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import dev.galacticraft.dynamicdimensions.impl.DynamicDimensionRegistryImpl;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.storage.WorldData;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
|
||||
/** Give Dynamic Dimensions the saved Age seed before it constructs the level and its chunk RandomState. */
|
||||
@Mixin(value = DynamicDimensionRegistryImpl.class, remap = false)
|
||||
public abstract class AgeDimensionSeedMixin {
|
||||
@ModifyVariable(method = "createDynamicLevel(Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/world/level/dimension/LevelStem;Lnet/minecraft/server/level/ServerLevel;)Lnet/minecraft/server/level/ServerLevel;",
|
||||
at = @At("STORE"), ordinal = 0, remap = false)
|
||||
private long rivencraft$useSavedAgeSeed(long defaultSeed, ResourceKey<Level> dimension,
|
||||
WorldData worldData, LevelStem stem, ServerLevel overworld) {
|
||||
return AgeRegistrySavedData.get(overworld).findByDimension(dimension.location())
|
||||
.map(age -> age.seed()).orElse(defaultSeed);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
@@ -18,6 +19,7 @@ import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
||||
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
@@ -59,7 +61,7 @@ public final class ModNetworking {
|
||||
}
|
||||
|
||||
public static void register(RegisterPayloadHandlersEvent event) {
|
||||
PayloadRegistrar registrar = event.registrar("15");
|
||||
PayloadRegistrar registrar = event.registrar("17");
|
||||
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
|
||||
ModNetworking::requestDescriptiveBookScreen);
|
||||
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
|
||||
@@ -85,6 +87,10 @@ public final class ModNetworking {
|
||||
ClientWritingDeskCatalog::handle);
|
||||
registrar.playToServer(SetBookBinderTitlePayload.TYPE, SetBookBinderTitlePayload.STREAM_CODEC,
|
||||
ModNetworking::setBookBinderTitle);
|
||||
registrar.playToServer(SetLinkModifierTitlePayload.TYPE, SetLinkModifierTitlePayload.STREAM_CODEC,
|
||||
ModNetworking::setLinkModifierTitle);
|
||||
registrar.playToServer(SetLinkModifierSeedPayload.TYPE, SetLinkModifierSeedPayload.STREAM_CODEC,
|
||||
ModNetworking::setLinkModifierSeed);
|
||||
}
|
||||
|
||||
private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) {
|
||||
@@ -95,6 +101,21 @@ public final class ModNetworking {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setLinkModifierTitle(SetLinkModifierTitlePayload payload, IPayloadContext context) {
|
||||
if (context.player() instanceof ServerPlayer player
|
||||
&& player.containerMenu instanceof LinkModifierMenu menu
|
||||
&& menu.containerId == payload.containerId()) {
|
||||
menu.setTitle(player, payload.title());
|
||||
}
|
||||
}
|
||||
|
||||
private static void setLinkModifierSeed(SetLinkModifierSeedPayload payload, IPayloadContext context) {
|
||||
if (context.player() instanceof ServerPlayer player
|
||||
&& player.containerMenu instanceof LinkModifierMenu menu
|
||||
&& menu.containerId == payload.containerId())
|
||||
menu.setSeed(player, payload.seed());
|
||||
}
|
||||
|
||||
private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
|
||||
if (!(context.player() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
@@ -163,7 +184,9 @@ public final class ModNetworking {
|
||||
try {
|
||||
AgeReference reference = new AgeReference(UUID.randomUUID(), title);
|
||||
if (AgeDimensionService.resolve(player, reference, access.stack()
|
||||
.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY)).isEmpty()) {
|
||||
.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY),
|
||||
!access.stack().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
|
||||
.has(LinkProperties.DISABLE_PLATFORM)).isEmpty()) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
|
||||
return;
|
||||
}
|
||||
@@ -211,7 +234,7 @@ public final class ModNetworking {
|
||||
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (stack.is(ModItems.LINKING_BOOK.get()) && stack.has(ModDataComponents.LINK_TARGET.get())) {
|
||||
LinkingBookItem.useBoundBook(player, stack);
|
||||
LinkingBookItem.useHandheldBook(player, hand(payload.offhand()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +268,8 @@ public final class ModNetworking {
|
||||
}
|
||||
|
||||
if (stack.is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
DescriptiveBookItem.useWrittenBook(player, stack);
|
||||
if (payload.fromStand()) DescriptiveBookItem.useWrittenBook(player, stack);
|
||||
else DescriptiveBookItem.useHandheldBook(player, hand(payload.offhand()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +292,9 @@ public final class ModNetworking {
|
||||
LinkTarget target;
|
||||
if (payload.descriptiveBook() && stack.is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
target = age == null ? null : AgeDimensionService.resolve(player, age).orElse(null);
|
||||
target = age == null ? null : AgeDimensionService.resolve(player, age, OrderedWriting.EMPTY,
|
||||
!stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
|
||||
.has(LinkProperties.DISABLE_PLATFORM)).orElse(null);
|
||||
} else if (!payload.descriptiveBook() && stack.is(ModItems.LINKING_BOOK.get())) {
|
||||
target = stack.get(ModDataComponents.LINK_TARGET.get());
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record SetLinkModifierSeedPayload(int containerId, String seed) implements CustomPacketPayload {
|
||||
public static final Type<SetLinkModifierSeedPayload> TYPE = new Type<>(
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_link_modifier_seed"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, SetLinkModifierSeedPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.VAR_INT, SetLinkModifierSeedPayload::containerId,
|
||||
ByteBufCodecs.STRING_UTF8, SetLinkModifierSeedPayload::seed,
|
||||
SetLinkModifierSeedPayload::new);
|
||||
|
||||
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record SetLinkModifierTitlePayload(int containerId, String title) implements CustomPacketPayload {
|
||||
public static final Type<SetLinkModifierTitlePayload> TYPE = new Type<>(
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_link_modifier_title"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, SetLinkModifierTitlePayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.VAR_INT, SetLinkModifierTitlePayload::containerId,
|
||||
ByteBufCodecs.STRING_UTF8, SetLinkModifierTitlePayload::title,
|
||||
SetLinkModifierTitlePayload::new);
|
||||
|
||||
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
|
||||
}
|
||||
@@ -2,12 +2,13 @@ package net.dnorberg.rivencraft.portal;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Session-only portal cooldowns: tick counters must never be saved with players. */
|
||||
public final class PortalTravelCooldown {
|
||||
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() {}
|
||||
|
||||
@@ -15,16 +16,16 @@ public final class PortalTravelCooldown {
|
||||
return previous >= 0 && now >= previous && now - previous < TICKS;
|
||||
}
|
||||
|
||||
public static boolean blocked(ServerPlayer player, long now) {
|
||||
return blocked(now, LAST_USE.getOrDefault(player, -1L));
|
||||
public static boolean blocked(Entity entity, long now) {
|
||||
return blocked(now, LAST_USE.getOrDefault(entity, -1L));
|
||||
}
|
||||
|
||||
public static void mark(ServerPlayer player, long now) {
|
||||
LAST_USE.put(player, now);
|
||||
public static void mark(Entity entity, long now) {
|
||||
LAST_USE.put(entity, now);
|
||||
}
|
||||
|
||||
public static void forget(ServerPlayer player) {
|
||||
LAST_USE.remove(player);
|
||||
public static void forget(Entity entity) {
|
||||
LAST_USE.remove(entity);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
|
||||
@@ -7,34 +7,43 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||
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.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Defers block-collision travel until the movement packet that entered the portal has finished. */
|
||||
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,
|
||||
ItemStack book, long queuedTick) {}
|
||||
private record Travel(ServerPlayer player, Pending pending) {}
|
||||
private record Travel(Entity entity, Pending pending) {}
|
||||
|
||||
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) {
|
||||
PENDING.putIfAbsent(player, new Pending(source, portal.immutable(), receptacle.immutable(),
|
||||
PENDING.putIfAbsent(entity, new Pending(source, portal.immutable(), receptacle.immutable(),
|
||||
book.copyWithCount(1), now));
|
||||
}
|
||||
|
||||
public static void process(MinecraftServer server) {
|
||||
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()) {
|
||||
var entry = iterator.next();
|
||||
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
|
||||
// iterator over the pending map while changing the player's dimension.
|
||||
for (Travel travel : ready) {
|
||||
ServerPlayer player = travel.player();
|
||||
Entity entity = travel.entity();
|
||||
Pending pending = travel.pending();
|
||||
if (server.getPlayerList().getPlayer(player.getUUID()) != player
|
||||
|| !player.isAlive() || player.serverLevel() != pending.source()
|
||||
if ((entity instanceof ServerPlayer player
|
||||
&& server.getPlayerList().getPlayer(player.getUUID()) != player)
|
||||
|| entity.isRemoved() || !entity.isAlive() || entity.level() != pending.source()
|
||||
|| !(pending.source().getBlockEntity(pending.portal()) instanceof CrystalPortalBlockEntity portal)
|
||||
|| !pending.receptacle().equals(portal.receptacle())
|
||||
|| !(pending.source().getBlockEntity(pending.receptacle()) instanceof BookReceptacleBlockEntity holder)
|
||||
|| !ItemStack.isSameItemSameComponents(holder.getBook(), pending.book())) continue;
|
||||
if (pending.book().is(ModItems.LINKING_BOOK.get()))
|
||||
LinkingBookItem.usePortalBook(player, pending.book());
|
||||
else if (pending.book().is(ModItems.DESCRIPTIVE_BOOK.get()))
|
||||
DescriptiveBookItem.usePortalBook(player, pending.book());
|
||||
if (entity instanceof ServerPlayer player) {
|
||||
if (pending.book().is(ModItems.LINKING_BOOK.get()))
|
||||
LinkingBookItem.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 clear() { PENDING.clear(); }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
|
||||
@@ -31,6 +32,9 @@ public final class ModBlockEntities {
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<InkMixerBlockEntity>> INK_MIXER =
|
||||
BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder
|
||||
.of(InkMixerBlockEntity::new, ModBlocks.INK_MIXER.get()).build(null));
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<LinkModifierBlockEntity>> LINK_MODIFIER =
|
||||
BLOCK_ENTITIES.register("link_modifier", () -> BlockEntityType.Builder
|
||||
.of(LinkModifierBlockEntity::new, ModBlocks.LINK_MODIFIER.get()).build(null));
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<StarFissureBlockEntity>> STAR_FISSURE =
|
||||
BLOCK_ENTITIES.register("star_fissure", () -> BlockEntityType.Builder
|
||||
.of(StarFissureBlockEntity::new, ModBlocks.STAR_FISSURE.get()).build(null));
|
||||
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.BookBinderBlock;
|
||||
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||
import net.dnorberg.rivencraft.block.LinkModifierBlock;
|
||||
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
||||
import net.dnorberg.rivencraft.block.StarFissureBlock;
|
||||
import net.dnorberg.rivencraft.block.DecayBlock;
|
||||
@@ -40,6 +41,9 @@ public final class ModBlocks {
|
||||
"ink_mixer", InkMixerBlock::new,
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()
|
||||
);
|
||||
public static final DeferredBlock<LinkModifierBlock> LINK_MODIFIER = BLOCKS.registerBlock(
|
||||
"link_modifier", LinkModifierBlock::new,
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
||||
Block::new, BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK).mapColor(MapColor.COLOR_BLUE));
|
||||
public static final DeferredBlock<BookReceptacleBlock> BOOK_RECEPTACLE = BLOCKS.registerBlock("book_receptacle",
|
||||
|
||||
@@ -7,6 +7,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
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.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -27,9 +30,13 @@ public final class ModCreativeTabs {
|
||||
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
|
||||
output.accept(ModItems.LINKING_BOOK.get());
|
||||
output.accept(ModItems.NOTEBOOK.get());
|
||||
output.accept(ModItems.COLLATION_FOLDER.get());
|
||||
creativeSymbolPortfolios(parameters.holders()).forEach(output::accept);
|
||||
output.accept(ModItems.SEALED_NOTEBOOK.get());
|
||||
output.accept(ModItems.INK_VIAL.get());
|
||||
output.accept(ModItems.WRITING_DESK.get());
|
||||
output.accept(ModItems.INK_MIXER.get());
|
||||
output.accept(ModItems.LINK_MODIFIER.get());
|
||||
output.accept(ModItems.BOOK_BINDER.get());
|
||||
output.accept(ModItems.CRYSTAL_BLOCK.get());
|
||||
output.accept(ModItems.BOOK_RECEPTACLE.get());
|
||||
@@ -56,14 +63,28 @@ public final class ModCreativeTabs {
|
||||
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
||||
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());
|
||||
categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
|
||||
int index = CATEGORY_ORDER.indexOf(id.getPath());
|
||||
return index < 0 ? CATEGORY_ORDER.size() : index;
|
||||
}).thenComparing(ResourceLocation::toString));
|
||||
return List.copyOf(categories);
|
||||
}
|
||||
|
||||
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
|
||||
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('/', '_');
|
||||
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
|
||||
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
public final class ModEntities {
|
||||
private static final DeferredRegister<EntityType<?>> ENTITIES =
|
||||
DeferredRegister.create(Registries.ENTITY_TYPE, Rivencraft.MOD_ID);
|
||||
|
||||
public static final DeferredHolder<EntityType<?>, EntityType<DroppedBookEntity>> DROPPED_BOOK =
|
||||
ENTITIES.register("dropped_book", () -> EntityType.Builder
|
||||
.<DroppedBookEntity>of(DroppedBookEntity::new, MobCategory.MISC)
|
||||
.sized(0.65F, 0.4F)
|
||||
.clientTrackingRange(10)
|
||||
.updateInterval(20)
|
||||
.build(Rivencraft.MOD_ID + ":dropped_book"));
|
||||
|
||||
private ModEntities() {
|
||||
}
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
ENTITIES.register(bus);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.NotebookItem;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
|
||||
import net.dnorberg.rivencraft.item.SealedNotebookItem;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
@@ -35,6 +38,12 @@ public final class ModItems {
|
||||
NotebookItem::new,
|
||||
new Item.Properties().stacksTo(1)
|
||||
);
|
||||
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
|
||||
"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(
|
||||
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
|
||||
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
||||
"ink_vial",
|
||||
InkVialItem::new,
|
||||
@@ -43,6 +52,7 @@ public final class ModItems {
|
||||
public static final DeferredItem<BlockItem> WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK);
|
||||
public static final DeferredItem<BlockItem> BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER);
|
||||
public static final DeferredItem<BlockItem> INK_MIXER = ITEMS.registerSimpleBlockItem(ModBlocks.INK_MIXER);
|
||||
public static final DeferredItem<BlockItem> LINK_MODIFIER = ITEMS.registerSimpleBlockItem(ModBlocks.LINK_MODIFIER);
|
||||
public static final DeferredItem<BlockItem> CRYSTAL_BLOCK = ITEMS.registerSimpleBlockItem(ModBlocks.CRYSTAL_BLOCK);
|
||||
public static final DeferredItem<BlockItem> BOOK_RECEPTACLE = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_RECEPTACLE);
|
||||
public static final DeferredItem<BlockItem> BUDDING_CRYSTAL = ITEMS.registerSimpleBlockItem(ModBlocks.BUDDING_CRYSTAL);
|
||||
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
@@ -24,6 +25,8 @@ public final class ModMenus {
|
||||
MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new));
|
||||
public static final DeferredHolder<MenuType<?>, MenuType<InkMixerMenu>> INK_MIXER =
|
||||
MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new));
|
||||
public static final DeferredHolder<MenuType<?>, MenuType<LinkModifierMenu>> LINK_MODIFIER =
|
||||
MENUS.register("link_modifier", () -> IMenuTypeExtension.create(LinkModifierMenu::new));
|
||||
|
||||
private ModMenus() {
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
/**
|
||||
* Number components from Mystcraft Legacy's ModWords#constructNumber.
|
||||
* Legacy registers 0–25; larger Rivencraft tab numbers use positional base 25.
|
||||
* Positional base-25 numerals using Mystcraft Legacy's 0–24 digit components.
|
||||
* Components within one digit are layered in the same square, not laid side by side.
|
||||
*/
|
||||
public final class SymbolNumerals {
|
||||
private SymbolNumerals() {}
|
||||
@@ -19,7 +19,7 @@ public final class SymbolNumerals {
|
||||
}
|
||||
|
||||
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};
|
||||
int fifths = digit / 5;
|
||||
int remainder = digit % 5;
|
||||
|
||||
@@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@@ -24,7 +25,7 @@ public final class WritingDeskCatalog {
|
||||
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
|
||||
ItemStack collection = desk.getItem(slot);
|
||||
if (collection.is(ModItems.PAGE.get())) add(unique, collection);
|
||||
if (collection.is(ModItems.NOTEBOOK.get())) {
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
List<ItemStack> pages = collection.getOrDefault(
|
||||
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
pages.forEach(page -> add(unique, page));
|
||||
|
||||
@@ -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}}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": { "model": "rivencraft:block/link_modifier" },
|
||||
"facing=east": { "model": "rivencraft:block/link_modifier", "y": 90 },
|
||||
"facing=south": { "model": "rivencraft:block/link_modifier", "y": 180 },
|
||||
"facing=west": { "model": "rivencraft:block/link_modifier", "y": 270 }
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,31 @@
|
||||
"item.rivencraft.link_property.disarm": "Disarm",
|
||||
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
||||
"item.rivencraft.link_property.intra_linking": "Intra Linking",
|
||||
"item.rivencraft.link_property.intra_linking_only": "Intra-Linking Only",
|
||||
"item.rivencraft.link_property.relative_link": "Relative Link",
|
||||
"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.notebook": "Notebook",
|
||||
"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",
|
||||
"block.rivencraft.link_modifier": "Link Modifier",
|
||||
"container.rivencraft.link_modifier": "Link Modifier",
|
||||
"screen.rivencraft.link_modifier.title": "Book title",
|
||||
"screen.rivencraft.link_modifier.seed": "Age seed",
|
||||
"screen.rivencraft.link_modifier.seed_warning": "Changing the seed preserves existing chunks but changes newly generated terrain. Press Enter to apply. The Age must be empty.",
|
||||
"message.rivencraft.link_modifier.seed_changed": "Age seed changed. Existing chunks remain; newly generated chunks use the new seed.",
|
||||
"message.rivencraft.link_modifier.seed_failed": "Could not change seed. Use a signed whole number and ensure no players are in the Age.",
|
||||
"screen.rivencraft.link_modifier.retire": "Retire Age",
|
||||
"screen.rivencraft.link_modifier.confirm": "Confirm!",
|
||||
"message.rivencraft.link_modifier.delete.retired": "Age retired. Its terrain was archived outside the active world.",
|
||||
"message.rivencraft.link_modifier.delete.already_dead": "This Age is already retired.",
|
||||
"message.rivencraft.link_modifier.delete.occupied": "Cannot retire an Age while players are inside it.",
|
||||
"message.rivencraft.link_modifier.delete.unload_failed": "Could not unload this Age; its data is unchanged.",
|
||||
"message.rivencraft.link_modifier.delete.storage_failed": "Could not archive this Age's data; it was not retired.",
|
||||
"item.rivencraft.notebook.pages": "Pages: %s/%s",
|
||||
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
|
||||
"item.rivencraft.ink_vial": "Ink Vial",
|
||||
@@ -88,6 +111,7 @@
|
||||
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
|
||||
"message.rivencraft.linking_book.unsafe": "No safe destination was found near the link",
|
||||
"message.rivencraft.linking_book.intra_required": "This book needs an Intra Linking panel for same-dimension travel",
|
||||
"message.rivencraft.linking_book.intra_only": "This book can only link within its current dimension",
|
||||
"message.rivencraft.descriptive_book.unwritten": "This Descriptive Book is unwritten",
|
||||
"message.rivencraft.descriptive_book.created": "Created Age: %s",
|
||||
"message.rivencraft.descriptive_book.unavailable": "That Age is unavailable",
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"parent": "block/block",
|
||||
"textures": {
|
||||
"particle": "rivencraft:block/link_modifier_base",
|
||||
"0": "rivencraft:block/link_modifier_base",
|
||||
"1": "rivencraft:block/link_modifier_details"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [ 0.0, 0.0, 0.0 ],
|
||||
"to": [ 16.0, 12.0, 16.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#0", "uv": [ 0.0, 4.0, 4.0, 7.0 ] },
|
||||
"east": { "texture": "#0", "uv": [ 4.0, 4.0, 8.0, 7.0 ] },
|
||||
"south": { "texture": "#0", "uv": [ 8.0, 4.0, 12.0, 7.0 ] },
|
||||
"west": { "texture": "#0", "uv": [ 12.0, 4.0, 16.0, 7.0 ] },
|
||||
"up": { "texture": "#0", "uv": [ 4.0, 0.0, 8.0, 4.0 ] },
|
||||
"down": { "texture": "#0", "uv": [ 8.0, 0.0, 12.0, 4.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 1.5, 12.0, 11.0 ],
|
||||
"to": [ 3.5, 13.0, 13.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 3.7, 12.0, 11.0 ],
|
||||
"to": [ 5.7, 13.0, 13.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 5.9, 12.0, 11.0 ],
|
||||
"to": [ 7.9, 13.0, 13.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 8.1, 12.0, 11.0 ],
|
||||
"to": [ 10.1, 13.0, 13.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 10.3, 12.0, 11.0 ],
|
||||
"to": [ 12.3, 13.0, 13.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 12.5, 12.0, 11.0 ],
|
||||
"to": [ 14.5, 13.0, 13.0 ],
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 2.0, 9.4, 1.0 ],
|
||||
"to": [ 14.0, 12.4, 8.0 ],
|
||||
"rotation": { "origin": [ 8.0, 8.0, 8.0 ], "axis": "x", "angle": 22.5 },
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 0.0, 4.0, 6.0, 5.5 ] },
|
||||
"east": { "texture": "#1", "uv": [ 6.0, 4.0, 9.5, 5.5 ] },
|
||||
"south": { "texture": "#1", "uv": [ 0.0, 6.0, 6.0, 7.5 ] },
|
||||
"west": { "texture": "#1", "uv": [ 6.0, 6.0, 9.5, 7.5 ] },
|
||||
"up": { "texture": "#1", "uv": [ 0.0, 0.0, 6.0, 3.5 ] },
|
||||
"down": { "texture": "#1", "uv": [ 6.0, 0.0, 12.0, 3.5 ] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [ 4.0, 11.5, 3.5 ],
|
||||
"to": [ 12.0, 15.5, 5.5 ],
|
||||
"rotation": { "origin": [ 8.0, 8.0, 8.0 ], "axis": "x", "angle": 22.5 },
|
||||
"faces": {
|
||||
"north": { "texture": "#1", "uv": [ 1.0, 9.0, 5.0, 11.0 ] },
|
||||
"east": { "texture": "#1", "uv": [ 0.0, 8.0, 1.0, 11.0 ] },
|
||||
"south": { "texture": "#1", "uv": [ 6.0, 9.0, 10.0, 11.0 ] },
|
||||
"west": { "texture": "#1", "uv": [ 5.0, 9.0, 6.0, 11.0 ] },
|
||||
"up": { "texture": "#1", "uv": [ 1.0, 8.0, 5.0, 9.0 ] },
|
||||
"down": { "texture": "#1", "uv": [ 5.0, 8.0, 9.0, 9.0 ] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": { "layer0": "rivencraft:item/collation_folder" }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "parent": "rivencraft:block/link_modifier" }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": { "layer0": "rivencraft:item/sealed_notebook" }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": { "layer0": "rivencraft:item/symbol_portfolio" }
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 303 B |
|
After Width: | Height: | Size: 730 B |
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "rivencraft:link_modifier" }] }]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
{ "type": "minecraft:item", "name": "minecraft:leather", "weight": 5,
|
||||
"functions": [{ "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 1, "max": 3 } }] },
|
||||
{ "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 4 },
|
||||
{ "type": "minecraft:item", "name": "minecraft:book", "weight": 3 },
|
||||
{ "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 20 },
|
||||
{ "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 }
|
||||
]
|
||||
},
|
||||
@@ -17,7 +17,7 @@
|
||||
"rolls": { "type": "minecraft:uniform", "min": 1, "max": 2 },
|
||||
"entries": [
|
||||
{ "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 3 },
|
||||
{ "type": "minecraft:item", "name": "minecraft:book", "weight": 2 },
|
||||
{ "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 2 },
|
||||
{ "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"package": "net.dnorberg.rivencraft.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
"AgeDimensionSeedMixin",
|
||||
"AgeDungeonPlacementMixin",
|
||||
"AgeStructureBiomeMixin",
|
||||
"MobDropChanceAccessor",
|
||||
"StructurePlacementAccessor"
|
||||
],
|
||||
"client": [
|
||||
|
||||