Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23b057f51 | ||
|
|
9ab8d3de4a | ||
|
|
56005ac512 |
No files matched your search
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 854 KiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 592 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 673 KiB |
@@ -83,6 +83,12 @@ Age must retain its identity, seed, chunks and access across save/restart.
|
||||
identify the derived files. Prefer the smallest backend Rivencraft needs
|
||||
over a general-purpose library fork.
|
||||
|
||||
For gameplay systems, treat the 1.21.1 Rivencraft source and assets as the
|
||||
porting baseline. Carry across unchanged rules, probabilities, recipes, and
|
||||
textures directly; adapt only the Minecraft/NeoForge APIs that changed in
|
||||
26.3. Record deliberate behavior gaps (such as prototype Age generation)
|
||||
instead of silently replacing established mechanics with new ones.
|
||||
|
||||
The release gate is a 26.3 integrated-client **and** dedicated-server matrix:
|
||||
create two Ages with distinct seeds, explore chunks, travel while another
|
||||
client remains online, reconnect, restart, revisit saved chunks, reseed for
|
||||
@@ -381,6 +387,99 @@ uses the acknowledged-catalog travel gate. This is not authored Page binding
|
||||
or a live destination preview. The player confirmed the Descriptive Book
|
||||
binding and travel interaction in game on 2026-10-29.
|
||||
|
||||
The following `1.0.1.4-alpha.1` development iteration registers a small
|
||||
Rivencraft creative tab for the two Books, Crystal Block, and Crystal Shard.
|
||||
It builds and passes the dedicated-server Age smoke test. The player confirmed
|
||||
the tab and four entries in game on 2026-10-29. No Page categories or
|
||||
unported items are advertised in the 26.3 tab.
|
||||
|
||||
The next local `1.0.1.5-alpha.1` iteration selects Legacy's unwritten
|
||||
Linking Book sprite until a target is bound, then the bound sprite. It also
|
||||
restores the four-Crystal-Shard crafting recipe for a Crystal Block. Both
|
||||
changes passed the player's in-game visual/crafting check on 2026-10-29.
|
||||
|
||||
The local `1.0.1.6-alpha.1` iteration ports Budding Crystal and the four
|
||||
blue crystal growth stages, reusing the 1.21.1 sprites and vanilla amethyst
|
||||
growth/harvest rules. Crystal clusters yield Shards, but natural crystal
|
||||
formation generation remains unported. Test growth with a Creative or
|
||||
command-placed Budding Crystal; the player passed the in-game check.
|
||||
|
||||
The local `1.0.1.7-alpha.1` iteration adds one persistent Page item with
|
||||
unwritten and Link Panel variants. It reuses the existing Page/Link Panel
|
||||
artwork and exposes only the Link Panel variant in its own creative tab,
|
||||
matching the 1.21.1 separation of Link Pages from symbol pages. Blank Pages
|
||||
remain command-only bases; the Ink Mixer, Book Binder and symbol writing are
|
||||
not yet ported. The player passed the in-game visual and persistence check.
|
||||
|
||||
The local `1.0.1.8-alpha.1` iteration ports the Ink Vial item, its animated
|
||||
sprite, and the black-dye/glass-bottle/water-bucket crafting recipe. Using
|
||||
it with paper in the other hand prepares a Link Panel at the current 1.21.1
|
||||
default cost of 2 ink from a 16-use vial. Ink remaining persists per vial;
|
||||
an empty vial yields a glass bottle. The Ink Mixer, configurable ink cost,
|
||||
and Book Binder remain unported. The player passed the in-game craft/use check.
|
||||
|
||||
The local `1.0.1.9-alpha.1` iteration ports the special shapeless leather +
|
||||
Link Panel recipe for an unwritten Linking Book. The 26.3 recipe serializer
|
||||
rejects blank Pages and extra ingredients and preserves a panel's custom name
|
||||
without carrying its Page identity onto the Book. No Link Panel modifiers are
|
||||
ported yet. The player passed the in-game crafting check on 2026-10-29. The
|
||||
full Book Binder GUI and Age authoring remain the next larger port step.
|
||||
|
||||
The local `1.0.1.10-alpha.1` iteration adds the first three Link Panel
|
||||
modifiers as a prerequisite to Binder authoring. All eight combinations of
|
||||
Disarm, Maintain Momentum, and Intra-Linking are testable in the Link Pages
|
||||
creative tab and persist as a synced component through leather crafting and
|
||||
book binding. The server uses the book's flags when linking: Intra-Linking
|
||||
allows same-dimension travel, Maintain Momentum rotates horizontal velocity
|
||||
to the destination yaw, and Disarm drops player inventory at the departure
|
||||
point only after a successful teleport. The Ink Mixer, additional Link Modifier
|
||||
flags, and Descriptive Book panel propagation are not yet ported. In-game
|
||||
testing of each modifier and their combinations passed in game. This local slice
|
||||
must not be committed, pushed or published without the player's explicit request.
|
||||
|
||||
The next local `1.0.1.11-alpha.1` slice introduces the Book Binder block and
|
||||
its first server-authoritative binding workflow. Leather and a Link Panel
|
||||
produce a named, bound Descriptive Book for a random prototype Age; the output
|
||||
is persisted in the Binder until collected. The panel's Disarm and Maintain
|
||||
Momentum flags carry onto the book and apply after successful Age travel;
|
||||
Intra-Linking is inert for Descriptive Books. This is not yet symbol-page
|
||||
authoring or the full Legacy Binder interface. Build and dedicated-server
|
||||
smoke pass. The player passed basic Binder crafting and travel in game, but
|
||||
reported that typing E closed the GUI and asked us to reuse the 1.21.1 GUI
|
||||
more faithfully. No
|
||||
commit, push, or publication without an explicit request during alpha.
|
||||
|
||||
The local `1.0.1.12-alpha.1` follow-up ports the existing Binder layout's
|
||||
top title field, flank slots, center Page strip, and pulsing red missing-panel
|
||||
cue. It persists the title on the block entity and consumes focused keyboard
|
||||
events before the container can treat E as inventory-close. Click the output
|
||||
once to bind and again to collect the result. The full authored-symbol strip
|
||||
and 1.21.1 grammar still await later port slices. The same slice adapts the
|
||||
existing 1.21.1 Ink Mixer block, persistent basin, additive probabilities,
|
||||
16-ink-per-panel exchange, virtual output, Legacy atlas and animated glyph to
|
||||
the 26.3 storage, menu and GUI APIs. The build and dedicated-server Age smoke
|
||||
pass. In-game Binder and Mixer verification is
|
||||
pending; do not commit, push, or publish without explicit authorization.
|
||||
|
||||
The local `1.0.1.13-alpha.1` iteration moves the Binder's freshly assembled
|
||||
book directly to the cursor so one output click both binds and takes it. Its
|
||||
central strip now displays five Legacy page thumbnails, while only the Link
|
||||
Panel is currently writable. The Mixer recomputes its virtual output in the
|
||||
same server click as an input change to address an intermittent first-attempt
|
||||
delay. The user's second Mixer attempt succeeded but exposed a cost mismatch:
|
||||
the Mixer now spends one ink and one paper per Link Panel and resets its
|
||||
additive chances on output pickup. Build passes; in-game retest is
|
||||
pending. This alpha remains local until explicitly authorized for release.
|
||||
|
||||
The local `1.0.1.14-alpha.1` visual follow-up uses the new Binder dev images
|
||||
as a reference: the page strip now grows only when a Link Panel is present or
|
||||
carried, its required-panel cue appears above the strip, and the first page
|
||||
can be clicked to place/take the panel. The stored page has a tooltip. The
|
||||
rest of the 1.21.1 strip (symbol pages and scrolling) depends on porting the
|
||||
symbol-page data and authoring pipeline, not just drawing more placeholders.
|
||||
The local `1.0.1.15-alpha.1` follow-up changes the missing-panel cue to the
|
||||
Link Panel's 16×16 item icon while preserving its red pulse and tooltip.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
|
||||
@@ -1,15 +1,157 @@
|
||||
# Minecraft 26.3 port workspace
|
||||
# Minecraft 26.3 beta port workspace
|
||||
|
||||
This is an **experimental alpha port** for issues #97–#104, not gameplay parity
|
||||
with the Minecraft 1.21.1 release. It registers basic items and a runtime Age
|
||||
prototype with persisted seeds. The operator-only `rivencraft_port age`
|
||||
commands create and inspect separate vanilla-terrain levels. The current
|
||||
development slice can name an unbound Descriptive Book to create a prototype
|
||||
Age, then enter it from the book GUI; the player verified this interaction in game.
|
||||
Named Linking Book binding/travel works as a limited probe. Authored
|
||||
generation, most content, recipes, workstations, portal systems, modifiers and
|
||||
the destination preview renderer remain unported. Use a test world and keep a
|
||||
backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
||||
with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
|
||||
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
|
||||
author Standard, Amplified, or Island terrain with supported biome-distribution
|
||||
pages. Other terrain families, feature/material effects, portals, and much of
|
||||
the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
|
||||
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
|
||||
The entries below record how the port was built, including earlier alpha-only
|
||||
limitations that were addressed by later slices.
|
||||
|
||||
The creative tab contains the two Books, Crystal Block and Crystal Shard;
|
||||
the player verified these in game. Unbound Linking Books use the Legacy
|
||||
sprite, and four Crystal Shards craft into a Crystal Block; both were also
|
||||
verified in game. The current local iteration adds Budding Crystal and all
|
||||
four blue crystal growth stages with vanilla-style harvesting. Natural crystal
|
||||
formation generation is not yet ported; use Creative or commands to obtain a
|
||||
Budding Crystal for this test. The player verified the growth chain in game.
|
||||
The next local slice adds the Link Panel Page as a separate creative entry,
|
||||
using the existing item texture. Blank Pages remain unavailable in Creative;
|
||||
the player verified the Link Panel's appearance in game. Ink Vials now use
|
||||
the existing crafting recipe and animated sprite. With paper in the other
|
||||
hand, an Ink Vial prepares a Link Panel for two of its sixteen ink uses, as
|
||||
in the current 1.21.1 default. The player verified the Ink Vial interaction
|
||||
in game. Symbol writing remains unported.
|
||||
A prepared Link Panel can now be combined shapelessly with leather to craft
|
||||
an unwritten Linking Book, as in 1.21.1. This custom recipe rejects a blank
|
||||
Page and extra ingredients. The first Book Binder slice now assembles random-Age
|
||||
Descriptive Books from leather and a Link Panel. Authored pages remain later.
|
||||
|
||||
The next local `1.0.1.10-alpha.1` slice exposes all eight combinations of
|
||||
plain, Disarm, Maintain Momentum, and Intra-Linking Link Panels in the Link
|
||||
Pages creative tab. Leather crafting preserves those flags on the Linking
|
||||
Book. Intra-Linking permits same-dimension travel; Maintain Momentum rotates
|
||||
horizontal speed to the destination yaw; Disarm leaves the player's inventory
|
||||
at the departure point after a successful teleport. These are development
|
||||
variants until the Ink Mixer is ported; other Link Panel modifiers are not
|
||||
implemented in 26.3. The player passed this modifier slice in game.
|
||||
|
||||
The local `1.0.1.11-alpha.1` slice adds a craftable Book Binder with persistent
|
||||
leather, Link Panel, and output slots. Name an Age in its GUI and press Bind;
|
||||
the server creates a random prototype Age and places its completed Descriptive
|
||||
Book in the output. Disarm and Maintain Momentum carry from the panel into the
|
||||
book; Intra-Linking has no effect on Age creation. Symbol-page authoring and
|
||||
the Ink Mixer remain unported. This slice passes the build and dedicated-server
|
||||
smoke test. The player passed crafting and travel in game, but found that E
|
||||
closed the focused title field and the provisional layout did not match the
|
||||
existing 1.21.1 Binder GUI.
|
||||
|
||||
The local `1.0.1.12-alpha.1` follow-up reuses the 1.21.1 Binder layout: title
|
||||
bar, cover and output at opposite sides, and a central Link Panel page strip.
|
||||
The title persists in the block, the missing-panel cue pulses red, and a
|
||||
focused title field consumes inventory/hotbar keys. Click the output once to
|
||||
bind, then click again to take the completed book. Authored symbol strips and
|
||||
the full 1.21.1 Binder grammar remain unported. This same slice also ports the
|
||||
1.21.1 Ink Mixer block, menu, animation, modifier additives, 16-ink-per-panel
|
||||
cost, and automatic vial exchange. The creative tab includes it and its
|
||||
vanilla-shaped recipe is available. This slice needs an in-game Binder visual/
|
||||
keyboard check and Ink Mixer interaction check.
|
||||
|
||||
The local `1.0.1.13-alpha.1` iteration makes Binder assembly place the new
|
||||
Descriptive Book directly on the cursor with one click. It also renders the
|
||||
five-page strip from the 1.21.1 GUI and refreshes the Mixer's virtual result
|
||||
in the same server click that changes paper or ink. The Mixer now consumes
|
||||
one ink unit and one paper per Link Panel (16 panels per full vial); additive
|
||||
chances reset when a panel is taken. In-game retest of the first-update
|
||||
behavior and Binder visual/click flow is pending.
|
||||
|
||||
The local `1.0.1.14-alpha.1` follow-up compares the current 26.3 Binder against
|
||||
the new dev-image sequence. The Binder no longer paints five permanently empty
|
||||
pages: the first strip page appears when a Link Panel is inserted or carried,
|
||||
and the missing-panel cue remains above it. Clicking that page inserts/removes
|
||||
the Link Panel using the same server-owned cursor flow as 1.21.1. Its tooltip
|
||||
also shows the stored panel. Multi-page symbol writing and strip scrolling
|
||||
cannot be exercised yet because symbol pages are still unported to 26.3.
|
||||
|
||||
The local `1.0.1.15-alpha.1` Binder polish uses the Link Panel's actual 16×16
|
||||
item icon for the missing-panel cue instead of a full-size page sprite. The
|
||||
red pulse and tooltip remain confined to that icon.
|
||||
|
||||
The local `1.0.1.16-alpha.1` slice adds the bundled 1.21.1 symbol-page
|
||||
identities to the 26.3 creative menu, grouped by category. Their names and
|
||||
descriptions are reused from the 1.21.1 translations, and the Page item now
|
||||
persists each symbol ID. These pages currently use the generic symbol-page
|
||||
artwork; the Legacy-style runtime glyph compositor has not been ported.
|
||||
They cannot yet be inserted into the Binder or change Age generation. This is
|
||||
the page-data foundation for the next authored-writing slice.
|
||||
|
||||
The local `1.0.1.17-alpha.1` follow-up derives biome pages from the active
|
||||
26.3 biome registry instead of only the older bundled list. It excludes the
|
||||
TerraBlender staging placeholder and separates cave/Nether biomes. Full-block
|
||||
materials and true source-fluid blocks also receive registry-derived pages,
|
||||
covering eligible new and modded blocks. Five 26.3 feature symbols cover
|
||||
poplar trees, fallen poplars, shelf mushrooms, red shrubs, and abandoned
|
||||
camps. These feature pages are identities only until their Age-generation
|
||||
handlers are ported. Biome, material, and feature pages still use generic
|
||||
artwork and cannot yet be placed in the Binder.
|
||||
|
||||
The local `1.0.1.18-alpha.1` follow-up restores the 1.21.1 animated redaction
|
||||
for Corrupted Page names and descriptions. It does not yet port the dynamic
|
||||
glyph artwork used for other symbol pages.
|
||||
|
||||
The local `1.0.1.19-alpha.1` slice ports the runtime glyph compositor for
|
||||
symbol Page items. It uses the shared Legacy component sheet and page
|
||||
parchment with a bounded 256-texture client cache; generated textures are
|
||||
released on resource reload. Registry-derived biome and material pages use
|
||||
the same deterministic four-word poem pattern as 1.21.1. No per-symbol PNGs
|
||||
are generated in the source tree. Binder insertion and authored Age generation
|
||||
are still unported.
|
||||
|
||||
The local `1.0.1.20-alpha.1` visual follow-up enlarges symbol artwork on Page
|
||||
items, slightly thickens the component strokes, and centers number glyphs at a
|
||||
larger size so they remain readable in inventory and creative-menu icons.
|
||||
|
||||
The local `1.0.1.21-alpha.1` slice ports the Binder's ordered, scrollable
|
||||
symbol-page strip. Pages persist in its block inventory and can be inserted,
|
||||
removed, and moved via the cursor or shift-click. A Binder with symbol pages
|
||||
cannot yet produce a book: authored Age compilation is the next port step, and
|
||||
silently producing a random Age would consume the player's writing incorrectly.
|
||||
|
||||
The local `1.0.1.22-alpha.1` visual follow-up gives large Binder pages the
|
||||
transparent, full-size Legacy glyph composition. Item icons retain their
|
||||
separate parchment-backed, reduced-size artwork.
|
||||
|
||||
The local `1.0.1.23-alpha.1` follow-up places Binder glyph overlays in a later
|
||||
26.3 GUI stratum than the page atlas, preventing texture batching from drawing
|
||||
the opaque page backgrounds over the dynamic artwork.
|
||||
|
||||
The local `1.0.1.24-alpha.1` follow-up renders Binder page glyphs through
|
||||
their working special item model, scaled to the page thumbnail. Direct GUI
|
||||
blitting of the generated texture remained blank in the 26.3 client.
|
||||
|
||||
The local `1.0.1.25-alpha.1` follow-up marks temporary Binder preview stacks
|
||||
for glyph-only special-model rendering. The full-size transparent artwork now
|
||||
uses the working item pipeline without drawing a second, smaller page outline.
|
||||
|
||||
The local `1.0.1.26-alpha.1` corrects the temporary preview component's codec
|
||||
registration, which NeoForge 26.3 requires during bootstrap.
|
||||
|
||||
The local `1.0.1.27-alpha.1` slice applies the first authored Age subset:
|
||||
Standard terrain plus at most one Biome page. The selected biome is persisted
|
||||
with the Age and used by a fixed-biome Overworld noise generator on creation
|
||||
and reload. Ordered page IDs are stored on the finished book. Other page
|
||||
combinations remain in the Binder and cannot yet be assembled, preventing
|
||||
silent loss of unsupported writing. Multi-biome distribution, other terrain
|
||||
profiles, and feature/material pages still need their 26.3 generation ports.
|
||||
|
||||
The `1.0.1.28`–`1.0.1.32` slices add Tiled, Grid, Native, and five scaled
|
||||
biome distributions; multiple authored Biome pages; Amplified and Island
|
||||
terrain; and saved terrain/biome plans across reload. A Binder or Ink Mixer
|
||||
beside another block no longer hides that block's faces. Unsupported authoring
|
||||
still stays in the Binder rather than silently producing an incorrect Age.
|
||||
|
||||
From the repository root:
|
||||
|
||||
@@ -25,7 +167,7 @@ Minecraft 1.21.1/Java 21 build:
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
The alpha JAR is under `ports/mc263/build/libs`. Continue the port and its
|
||||
The beta JAR is under `ports/mc263/build/libs`. Continue the port and its
|
||||
full dual-version validation in issues #98–#104 before calling 26.3 stable.
|
||||
|
||||
The dedicated development server uses `build/run-server`, with its own world
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.32-beta.1
|
||||
|
||||
This is the first 26.3 beta, focused on authored Age generation. It is not yet
|
||||
feature-complete with the separate Minecraft 1.21.1 release.
|
||||
|
||||
- Ported the Book Binder's ordered symbol-page strip and Legacy-inspired GUI,
|
||||
plus runtime symbol artwork, creative pages, and registry-derived biome pages.
|
||||
- Ported the Ink Mixer, its additive modifiers, and one-paper/one-ink-unit Link
|
||||
Panel creation.
|
||||
- Added authored Standard, Amplified, and Island terrain; fixed, Tiled, Grid,
|
||||
Native, and Tiny through Huge biome distributions; and persistent Age plans.
|
||||
- Fixed workstation block-face occlusion beside the Book Binder and Ink Mixer.
|
||||
|
||||
Feature/material writing, Cave/Flat/Void terrain, portals, and other 1.21.1
|
||||
systems are still being ported. Use a test world and keep a backup. Minecraft
|
||||
1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and
|
||||
Java 25.
|
||||
@@ -59,22 +59,131 @@ def generateModMetadata = tasks.register('generateModMetadata', ProcessResources
|
||||
sourceSets.main.resources.srcDir generateModMetadata
|
||||
neoForge.ideSyncTask generateModMetadata
|
||||
|
||||
// Keep the bundled page list in step with the 1.21.1 symbol definitions.
|
||||
abstract class GeneratePortSymbolIndex extends DefaultTask {
|
||||
@InputDirectory abstract DirectoryProperty getSymbolsDir()
|
||||
@InputDirectory abstract DirectoryProperty getPortSymbolsDir()
|
||||
@OutputDirectory abstract DirectoryProperty getIndexDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
File root = symbolsDir.get().asFile
|
||||
def definitions = new LinkedHashMap<String, Object>()
|
||||
java.nio.file.Files.walk(root.toPath()).withCloseable { paths ->
|
||||
paths.filter { java.nio.file.Files.isRegularFile(it) && it.toString().endsWith('.json') }
|
||||
.sorted().forEach { source ->
|
||||
String path = root.toPath().relativize(source).toString()
|
||||
.replace('\\', '/').replaceAll(/\.json$/, '')
|
||||
definitions["rivencraft:${path}"] = new groovy.json.JsonSlurper().parse(source.toFile())
|
||||
}
|
||||
}
|
||||
File portRoot = portSymbolsDir.get().asFile
|
||||
java.nio.file.Files.walk(portRoot.toPath()).withCloseable { paths ->
|
||||
paths.filter { java.nio.file.Files.isRegularFile(it) && it.toString().endsWith('.json') }
|
||||
.sorted().forEach { source ->
|
||||
String path = portRoot.toPath().relativize(source).toString()
|
||||
.replace('\\', '/').replaceAll(/\.json$/, '')
|
||||
definitions["rivencraft:${path}"] = new groovy.json.JsonSlurper().parse(source.toFile())
|
||||
}
|
||||
}
|
||||
File index = new File(indexDir.get().asFile, 'assets/rivencraft/creative_symbol_pages.json')
|
||||
index.parentFile.mkdirs()
|
||||
index.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definitions)) + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortSymbolIndex = tasks.register('generatePortSymbolIndex', GeneratePortSymbolIndex) {
|
||||
symbolsDir.set(layout.projectDirectory.dir('../../src/main/resources/data/rivencraft/rivencraft/symbols'))
|
||||
portSymbolsDir.set(layout.projectDirectory.dir('src/main/resources/data/rivencraft/rivencraft/symbols'))
|
||||
indexDir.set(layout.buildDirectory.dir('generated/resources/creativeSymbols'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortSymbolIndex
|
||||
|
||||
// Use the established symbol names/descriptions without overwriting 26.3-only strings.
|
||||
abstract class GeneratePortLanguage extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getBaseLanguage()
|
||||
@InputFile abstract RegularFileProperty getPortLanguage()
|
||||
@OutputDirectory abstract DirectoryProperty getOutputDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
def base = new groovy.json.JsonSlurper().parse(baseLanguage.get().asFile) as Map
|
||||
def port = new groovy.json.JsonSlurper().parse(portLanguage.get().asFile) as Map
|
||||
base.putAll(port)
|
||||
File result = new File(outputDir.get().asFile, 'assets/rivencraft/lang/en_us.json')
|
||||
result.parentFile.mkdirs()
|
||||
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(base)) + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLanguage) {
|
||||
baseLanguage.set(layout.projectDirectory.file('../../src/main/resources/assets/rivencraft/lang/en_us.json'))
|
||||
portLanguage.set(layout.projectDirectory.file('port_en_us.json'))
|
||||
outputDir.set(layout.buildDirectory.dir('generated/resources/portLanguage'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortLanguage
|
||||
|
||||
tasks.named('processResources', ProcessResources).configure {
|
||||
// Reuse the existing sprite and base model without copying binary assets.
|
||||
from('../../src/main/resources') {
|
||||
include 'assets/rivencraft/textures/item/crystal_shard.png'
|
||||
include 'assets/rivencraft/textures/item/page.png'
|
||||
include 'assets/rivencraft/textures/item/page_link_panel.png'
|
||||
include 'assets/rivencraft/textures/item/page_symbol.png'
|
||||
include 'assets/rivencraft/models/item/page_symbol.json'
|
||||
include 'assets/rivencraft/textures/gui/symbolcomponents.png'
|
||||
include 'assets/rivencraft/models/item/page_link_panel.json'
|
||||
include 'assets/rivencraft/textures/item/ink_vial.png'
|
||||
include 'assets/rivencraft/textures/item/ink_vial.png.mcmeta'
|
||||
include 'assets/rivencraft/models/item/ink_vial.json'
|
||||
include 'assets/rivencraft/models/item/crystal_shard.json'
|
||||
include 'assets/rivencraft/textures/item/descriptive_book.png'
|
||||
include 'assets/rivencraft/models/item/descriptive_book.json'
|
||||
include 'assets/rivencraft/textures/item/linking_book.png'
|
||||
include 'assets/rivencraft/textures/item/linking_book_unwritten.png'
|
||||
include 'assets/rivencraft/models/item/linking_book_bound.json'
|
||||
include 'assets/rivencraft/textures/gui/bookui_cover.png'
|
||||
include 'assets/rivencraft/textures/gui/notebook_pages.png'
|
||||
include 'assets/rivencraft/textures/gui/bookui_pager.png'
|
||||
include 'assets/rivencraft/textures/block/crystal_block.png'
|
||||
include 'assets/rivencraft/textures/block/book_binder.png'
|
||||
include 'assets/rivencraft/textures/block/book_binder_press.png'
|
||||
include 'assets/rivencraft/textures/gui/book_binder_legacy.png'
|
||||
include 'assets/rivencraft/textures/gui/ink_mixer_legacy.png'
|
||||
include 'assets/rivencraft/textures/block/inkwell_base.png'
|
||||
include 'assets/rivencraft/textures/block/inkwell_cup.png'
|
||||
include 'assets/rivencraft/textures/block/ink.png'
|
||||
include 'assets/rivencraft/textures/block/ink_glass.png'
|
||||
include 'assets/rivencraft/models/block/ink_mixer.json'
|
||||
include 'assets/rivencraft/models/item/ink_mixer.json'
|
||||
include 'assets/rivencraft/blockstates/ink_mixer.json'
|
||||
include 'data/rivencraft/loot_table/blocks/ink_mixer.json'
|
||||
include 'assets/rivencraft/models/block/book_binder.json'
|
||||
include 'assets/rivencraft/models/item/book_binder.json'
|
||||
include 'assets/rivencraft/blockstates/book_binder.json'
|
||||
include 'data/rivencraft/loot_table/blocks/book_binder.json'
|
||||
include 'assets/rivencraft/models/block/crystal_block.json'
|
||||
include 'assets/rivencraft/blockstates/crystal_block.json'
|
||||
include 'assets/rivencraft/blockstates/budding_crystal.json'
|
||||
include 'assets/rivencraft/blockstates/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/medium_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/large_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/crystal_cluster.json'
|
||||
include 'assets/rivencraft/models/block/budding_crystal.json'
|
||||
include 'assets/rivencraft/models/block/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/block/medium_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/block/large_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/block/crystal_cluster.json'
|
||||
include 'assets/rivencraft/models/item/budding_crystal.json'
|
||||
include 'assets/rivencraft/models/item/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/item/medium_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/item/large_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/item/crystal_cluster.json'
|
||||
include 'assets/rivencraft/textures/block/small_crystal_bud.png'
|
||||
include 'assets/rivencraft/textures/block/medium_crystal_bud.png'
|
||||
include 'assets/rivencraft/textures/block/large_crystal_bud.png'
|
||||
include 'assets/rivencraft/textures/block/crystal_cluster.png'
|
||||
include 'data/rivencraft/loot_table/blocks/crystal_block.json'
|
||||
include 'data/minecraft/tags/block/mineable/pickaxe.json'
|
||||
include 'data/minecraft/tags/item/cluster_max_harvestables.json'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
|
||||
mod_id=rivencraft
|
||||
mod_name=Rivencraft
|
||||
mod_license=LGPL-3.0-or-later
|
||||
mod_version=1.0.1.3-alpha.1
|
||||
mod_version=1.0.1.32-beta.1
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"item.rivencraft.crystal_shard": "Crystal Shard",
|
||||
"item.rivencraft.descriptive_book": "Descriptive Book",
|
||||
"item.rivencraft.linking_book": "Linking Book",
|
||||
"item.rivencraft.page": "Page",
|
||||
"item.rivencraft.page.blank": "Blank Page",
|
||||
"item.rivencraft.page.link_panel": "Link Panel Page",
|
||||
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
|
||||
"item.rivencraft.page.symbol.port_status": "Binder currently supports Standard terrain and one Biome page; other effects are still being ported.",
|
||||
"item.rivencraft.link_property.disarm": "Disarm",
|
||||
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
||||
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
|
||||
"item.rivencraft.ink_vial": "Ink Vial",
|
||||
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
||||
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand, or place in an Ink Mixer.",
|
||||
"item.rivencraft.page.blank.port_status": "Base for future symbol writing; not yet usable in this port.",
|
||||
"item.rivencraft.page.link_panel.port_status": "Use with leather in a Book Binder to make a Descriptive Book.",
|
||||
"block.rivencraft.book_binder": "Book Binder",
|
||||
"container.rivencraft.book_binder": "Book Binder",
|
||||
"screen.rivencraft.book_binder.missing_panel": "Link Panel required",
|
||||
"screen.rivencraft.book_binder.authored_pending": "Writing not yet ported",
|
||||
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||
"screen.rivencraft.ink_mixer.additive_hint": "Click with an additive to mix it in",
|
||||
"screen.rivencraft.ink_mixer.chance": "%s: %s%%",
|
||||
"block.rivencraft.crystal_block": "Crystal Block",
|
||||
"block.rivencraft.budding_crystal": "Budding Crystal",
|
||||
"block.rivencraft.small_crystal_bud": "Small Crystal Bud",
|
||||
"block.rivencraft.medium_crystal_bud": "Medium Crystal Bud",
|
||||
"block.rivencraft.large_crystal_bud": "Large Crystal Bud",
|
||||
"block.rivencraft.crystal_cluster": "Crystal Cluster",
|
||||
"itemGroup.rivencraft": "Rivencraft",
|
||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||
"symbol.rivencraft.feature.poplar_trees": "Poplar Trees",
|
||||
"symbol.rivencraft.feature.poplar_trees.description": "Poplar tree generation symbol.",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees": "Fallen Poplar Trees",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Fallen poplar tree generation symbol.",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms": "Shelf Mushrooms",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms.description": "Shelf mushroom generation symbol.",
|
||||
"symbol.rivencraft.feature.red_shrubs": "Red Shrubs",
|
||||
"symbol.rivencraft.feature.red_shrubs.description": "Red shrub generation symbol.",
|
||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Abandoned camp structure generation symbol."
|
||||
}
|
||||
@@ -1,26 +1,47 @@
|
||||
package net.dnorberg.rivencraft;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.dnorberg.rivencraft.age.PortAgeCommands;
|
||||
import net.dnorberg.rivencraft.age.AgeCatalogSync;
|
||||
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
||||
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||
import net.dnorberg.rivencraft.registry.PortBiomeSources;
|
||||
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.neoforge.registries.DeferredBlock;
|
||||
import net.neoforged.neoforge.registries.DeferredItem;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
import net.neoforged.neoforge.common.extensions.IMenuTypeExtension;
|
||||
import java.util.function.Supplier;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import org.slf4j.Logger;
|
||||
@@ -32,6 +53,12 @@ public final class Rivencraft {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
||||
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
|
||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
||||
DeferredRegister.create(Registries.MENU, MOD_ID);
|
||||
private static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS =
|
||||
DeferredRegister.create(Registries.RECIPE_SERIALIZER, MOD_ID);
|
||||
private static final DeferredRegister.DataComponents COMPONENTS =
|
||||
DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MOD_ID);
|
||||
|
||||
@@ -41,20 +68,90 @@ public final class Rivencraft {
|
||||
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
|
||||
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
||||
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
||||
/** A present component distinguishes Link Panels from unwritten base Pages. */
|
||||
public static final Supplier<DataComponentType<Boolean>> LINK_PANEL_PAGE = COMPONENTS.registerComponentType(
|
||||
"link_panel_page", builder -> builder.persistent(Codec.BOOL)
|
||||
.networkSynchronized(ByteBufCodecs.BOOL));
|
||||
public static final Supplier<DataComponentType<Identifier>> SYMBOL_PAGE_ID = COMPONENTS.registerComponentType(
|
||||
"symbol_page_id", builder -> builder.persistent(Identifier.CODEC)
|
||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||
/** Ordered authored page IDs remain on the finished book for later port slices. */
|
||||
public static final Supplier<DataComponentType<java.util.List<Identifier>>> BOOK_SYMBOLS = COMPONENTS.registerComponentType(
|
||||
"book_symbols", builder -> builder.persistent(Identifier.CODEC.listOf())
|
||||
.networkSynchronized(ByteBufCodecs.<io.netty.buffer.ByteBuf, Identifier>list(252)
|
||||
.apply(Identifier.STREAM_CODEC)));
|
||||
/** Set only on temporary GUI stacks; NeoForge 26.3 requires a registered codec. */
|
||||
public static final Supplier<DataComponentType<Boolean>> PAGE_GLYPH_ONLY = COMPONENTS.registerComponentType(
|
||||
"page_glyph_only", builder -> builder.persistent(Codec.BOOL)
|
||||
.networkSynchronized(ByteBufCodecs.BOOL));
|
||||
public static final Supplier<DataComponentType<Integer>> INK_REMAINING = COMPONENTS.registerComponentType(
|
||||
"ink_remaining", builder -> builder.persistent(Codec.INT)
|
||||
.networkSynchronized(ByteBufCodecs.VAR_INT));
|
||||
public static final Supplier<DataComponentType<LinkProperties>> LINK_PROPERTIES = COMPONENTS.registerComponentType(
|
||||
"link_properties", builder -> builder.persistent(LinkProperties.CODEC)
|
||||
.networkSynchronized(LinkProperties.STREAM_CODEC));
|
||||
|
||||
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
||||
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
|
||||
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||
"book_binder", PortBookBinderBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
public static final DeferredItem<BlockItem> BOOK_BINDER_ITEM = ITEMS.registerSimpleBlockItem(BOOK_BINDER);
|
||||
public static final DeferredBlock<InkMixerBlock> INK_MIXER = BLOCKS.registerBlock(
|
||||
"ink_mixer", InkMixerBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
public static final DeferredItem<BlockItem> INK_MIXER_ITEM = ITEMS.registerSimpleBlockItem(INK_MIXER);
|
||||
public static final Supplier<BlockEntityType<PortBookBinderBlockEntity>> BOOK_BINDER_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("book_binder", () ->
|
||||
new BlockEntityType<>(PortBookBinderBlockEntity::new, BOOK_BINDER.get()));
|
||||
public static final Supplier<MenuType<PortBookBinderMenu>> BOOK_BINDER_MENU =
|
||||
MENUS.register("book_binder", () -> IMenuTypeExtension.create(PortBookBinderMenu::new));
|
||||
public static final Supplier<BlockEntityType<InkMixerBlockEntity>> INK_MIXER_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("ink_mixer", () ->
|
||||
new BlockEntityType<>(InkMixerBlockEntity::new, INK_MIXER.get()));
|
||||
public static final Supplier<MenuType<InkMixerMenu>> INK_MIXER_MENU =
|
||||
MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new));
|
||||
public static final DeferredItem<BlockItem> CRYSTAL_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_BLOCK);
|
||||
public static final DeferredBlock<BuddingCrystalBlock> BUDDING_CRYSTAL = BLOCKS.registerBlock(
|
||||
"budding_crystal", BuddingCrystalBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.BUDDING_AMETHYST).mapColor(MapColor.COLOR_BLUE));
|
||||
public static final DeferredItem<BlockItem> BUDDING_CRYSTAL_ITEM = ITEMS.registerSimpleBlockItem(BUDDING_CRYSTAL);
|
||||
public static final DeferredBlock<AmethystClusterBlock> SMALL_CRYSTAL_BUD = BLOCKS.registerBlock(
|
||||
"small_crystal_bud", properties -> new AmethystClusterBlock(3.0F, 4.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.SMALL_AMETHYST_BUD));
|
||||
public static final DeferredItem<BlockItem> SMALL_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(SMALL_CRYSTAL_BUD);
|
||||
public static final DeferredBlock<AmethystClusterBlock> MEDIUM_CRYSTAL_BUD = BLOCKS.registerBlock(
|
||||
"medium_crystal_bud", properties -> new AmethystClusterBlock(4.0F, 3.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.MEDIUM_AMETHYST_BUD));
|
||||
public static final DeferredItem<BlockItem> MEDIUM_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(MEDIUM_CRYSTAL_BUD);
|
||||
public static final DeferredBlock<AmethystClusterBlock> LARGE_CRYSTAL_BUD = BLOCKS.registerBlock(
|
||||
"large_crystal_bud", properties -> new AmethystClusterBlock(5.0F, 3.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.LARGE_AMETHYST_BUD));
|
||||
public static final DeferredItem<BlockItem> LARGE_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(LARGE_CRYSTAL_BUD);
|
||||
public static final DeferredBlock<AmethystClusterBlock> CRYSTAL_CLUSTER = BLOCKS.registerBlock(
|
||||
"crystal_cluster", properties -> new AmethystClusterBlock(7.0F, 3.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_CLUSTER));
|
||||
public static final DeferredItem<BlockItem> CRYSTAL_CLUSTER_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_CLUSTER);
|
||||
public static final DeferredItem<Item> CRYSTAL_SHARD = ITEMS.registerSimpleItem("crystal_shard");
|
||||
public static final DeferredItem<PageItem> PAGE = ITEMS.registerItem("page", PageItem::new);
|
||||
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
||||
"ink_vial", InkVialItem::new, properties -> properties.stacksTo(16));
|
||||
public static final DeferredItem<DescriptiveBookItem> DESCRIPTIVE_BOOK = ITEMS.registerItem(
|
||||
"descriptive_book", DescriptiveBookItem::new, properties -> properties.stacksTo(1));
|
||||
public static final DeferredItem<LinkingBookItem> LINKING_BOOK = ITEMS.registerItem(
|
||||
"linking_book", LinkingBookItem::new, properties -> properties.stacksTo(1));
|
||||
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
|
||||
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
|
||||
|
||||
public Rivencraft(IEventBus modEventBus) {
|
||||
BLOCKS.register(modEventBus);
|
||||
ITEMS.register(modEventBus);
|
||||
BLOCK_ENTITY_TYPES.register(modEventBus);
|
||||
MENUS.register(modEventBus);
|
||||
COMPONENTS.register(modEventBus);
|
||||
RECIPE_SERIALIZERS.register(modEventBus);
|
||||
ModCreativeTabs.register(modEventBus);
|
||||
PortBiomeSources.register(modEventBus);
|
||||
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
|
||||
@@ -17,7 +17,14 @@ final class AgeLevelIndex extends SavedData {
|
||||
private static final Codec<AgeLevelIndex> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.unboundedMap(Identifier.CODEC, Codec.LONG).fieldOf("ages").forGetter(index -> index.ages),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("retired", List.of())
|
||||
.forGetter(index -> List.copyOf(index.retired))
|
||||
.forGetter(index -> List.copyOf(index.retired)),
|
||||
Codec.unboundedMap(Identifier.CODEC, Identifier.CODEC).optionalFieldOf("biome_overrides", Map.of())
|
||||
.forGetter(index -> index.biomeOverrides),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortBiomePlan.CODEC).optionalFieldOf("biome_plans", Map.of())
|
||||
.forGetter(index -> index.biomePlans),
|
||||
Codec.unboundedMap(Identifier.CODEC,
|
||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -28,15 +35,27 @@ final class AgeLevelIndex extends SavedData {
|
||||
|
||||
private final Map<Identifier, Long> ages;
|
||||
private final Set<Identifier> retired;
|
||||
private final Map<Identifier, Identifier> biomeOverrides;
|
||||
private final Map<Identifier, PortBiomePlan> biomePlans;
|
||||
private final Map<Identifier, PortAgeTerrain> terrain;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
this.retired = new LinkedHashSet<>();
|
||||
this.biomeOverrides = new LinkedHashMap<>();
|
||||
this.biomePlans = new LinkedHashMap<>();
|
||||
this.terrain = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired) {
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
Map<Identifier, Identifier> biomeOverrides,
|
||||
Map<Identifier, PortBiomePlan> biomePlans,
|
||||
Map<Identifier, PortAgeTerrain> terrain) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||
this.terrain = new LinkedHashMap<>(terrain);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
@@ -51,23 +70,42 @@ final class AgeLevelIndex extends SavedData {
|
||||
return ages.get(id);
|
||||
}
|
||||
|
||||
void add(Identifier id, long seed) {
|
||||
PortBiomePlan biomePlan(Identifier id) {
|
||||
PortBiomePlan plan = biomePlans.get(id);
|
||||
if (plan != null) return plan;
|
||||
Identifier oldBiome = biomeOverrides.get(id);
|
||||
return oldBiome == null ? null : new PortBiomePlan(PortBiomePlan.Distribution.SINGLE, List.of(oldBiome));
|
||||
}
|
||||
|
||||
PortAgeTerrain terrain(Identifier id) {
|
||||
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
||||
}
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + id);
|
||||
}
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void retire(Identifier id) {
|
||||
if (ages.remove(id) == null) throw new IllegalArgumentException("Age not indexed: " + id);
|
||||
biomeOverrides.remove(id);
|
||||
biomePlans.remove(id);
|
||||
terrain.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed) {
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||
PortAgeTerrain ageTerrain) {
|
||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
/** Terrain variants currently backed by 26.3 vanilla noise settings. */
|
||||
public enum PortAgeTerrain {
|
||||
STANDARD,
|
||||
AMPLIFIED,
|
||||
ISLAND
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen authored biome choice; repeated entries retain their selection weight. */
|
||||
public record PortBiomePlan(Distribution distribution, List<Identifier> biomes) {
|
||||
public enum Distribution { NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE }
|
||||
|
||||
public static final Codec<PortBiomePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.xmap(Distribution::valueOf, Distribution::name).fieldOf("distribution")
|
||||
.forGetter(PortBiomePlan::distribution),
|
||||
Identifier.CODEC.listOf().fieldOf("biomes").forGetter(PortBiomePlan::biomes)
|
||||
).apply(instance, PortBiomePlan::new));
|
||||
|
||||
public PortBiomePlan {
|
||||
biomes = List.copyOf(biomes);
|
||||
if (distribution == Distribution.NATIVE && !biomes.isEmpty()
|
||||
|| distribution == Distribution.SINGLE && biomes.size() != 1
|
||||
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
|
||||
throw new IllegalArgumentException("Invalid authored biome distribution");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** 26.3 resolver port of the seeded 1.21.1 Mystcraft-style region distribution. */
|
||||
public final class PortScaledBiomeSource extends BiomeSource implements BiomeResolver {
|
||||
public static final MapCodec<PortScaledBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.LIST_CODEC.fieldOf("biomes").forGetter(source -> source.biomes),
|
||||
Codec.LONG.fieldOf("seed").forGetter(source -> source.seed),
|
||||
Codec.intRange(0, 4).fieldOf("scale").forGetter(source -> source.scale)
|
||||
).apply(instance, PortScaledBiomeSource::new));
|
||||
|
||||
private final HolderSet<Biome> biomes;
|
||||
private final long seed;
|
||||
private final int scale;
|
||||
private final int cellWidth;
|
||||
|
||||
public PortScaledBiomeSource(HolderSet<Biome> biomes, long seed, int scale) {
|
||||
if (biomes.size() < 2 || scale < 0 || scale > 4)
|
||||
throw new IllegalArgumentException("Scaled regions need two biomes and scale 0..4");
|
||||
this.biomes = biomes;
|
||||
this.seed = seed;
|
||||
this.scale = scale;
|
||||
// Quart coordinates; Tiny starts at 16 blocks and each scale doubles.
|
||||
this.cellWidth = 4 << scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return biomes.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z) {
|
||||
int baseX = Math.floorDiv(x, cellWidth);
|
||||
int baseZ = Math.floorDiv(z, cellWidth);
|
||||
int closestX = baseX;
|
||||
int closestZ = baseZ;
|
||||
double closestDistance = Double.POSITIVE_INFINITY;
|
||||
for (int cellX = baseX - 1; cellX <= baseX + 1; cellX++) {
|
||||
for (int cellZ = baseZ - 1; cellZ <= baseZ + 1; cellZ++) {
|
||||
long cellSeed = mix(seed, cellX, cellZ);
|
||||
double centerX = (cellX + 0.5D + jitter(cellSeed) * 0.8D) * cellWidth;
|
||||
double centerZ = (cellZ + 0.5D + jitter(mix64(cellSeed)) * 0.8D) * cellWidth;
|
||||
double deltaX = x - centerX;
|
||||
double deltaZ = z - centerZ;
|
||||
double distance = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestX = cellX;
|
||||
closestZ = cellZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
return biomes.get(Math.floorMod((int) mix(seed ^ 0x42494F4D45534CL,
|
||||
closestX, closestZ), biomes.size()));
|
||||
}
|
||||
|
||||
private static double jitter(long value) {
|
||||
return ((value >>> 11) * 0x1.0p-53) - 0.5D;
|
||||
}
|
||||
|
||||
private static long mix(long seed, int x, int z) {
|
||||
return mix64(seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL);
|
||||
}
|
||||
|
||||
private static long mix64(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
@@ -18,7 +20,12 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.TicketStorage;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
@@ -41,7 +48,8 @@ public final class RuntimeAgeBackend {
|
||||
AgeLevelIndex index = index(server);
|
||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||
try {
|
||||
load(server, age.getKey(), age.getValue());
|
||||
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
||||
index.terrain(age.getKey()));
|
||||
} catch (RuntimeException exception) {
|
||||
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
||||
}
|
||||
@@ -50,12 +58,21 @@ public final class RuntimeAgeBackend {
|
||||
|
||||
/** Creates a server level, then commits its identity only after successful construction. */
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed) {
|
||||
return create(server, id, seed, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed, PortBiomePlan biomePlan) {
|
||||
return create(server, id, seed, biomePlan, PortAgeTerrain.STANDARD);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
checkServerThread(server);
|
||||
ageName(id);
|
||||
AgeLevelIndex index = index(server);
|
||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||
ServerLevel level = load(server, id, seed);
|
||||
index.add(id, seed);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain);
|
||||
index.add(id, seed, biomePlan, terrain);
|
||||
// A newly bound book may be used immediately. Persist its Age identity
|
||||
// before publishing the catalog so a later crash cannot leave that
|
||||
// book pointing at a world omitted from the on-disk index.
|
||||
@@ -75,7 +92,7 @@ public final class RuntimeAgeBackend {
|
||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
long seed = requireSeed(server, id);
|
||||
ServerLevel level = load(server, id, seed);
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id));
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
return level;
|
||||
}
|
||||
@@ -85,18 +102,20 @@ public final class RuntimeAgeBackend {
|
||||
checkServerThread(server);
|
||||
AgeLevelIndex index = index(server);
|
||||
long previousSeed = requireSeed(server, id);
|
||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||
PortAgeTerrain terrain = index.terrain(id);
|
||||
if (previousSeed == seed) return;
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
try {
|
||||
if (loaded) load(server, id, seed);
|
||||
if (loaded) load(server, id, seed, biomePlan, terrain);
|
||||
index.reseed(id, seed);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException exception) {
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, previousSeed);
|
||||
load(server, id, previousSeed, biomePlan, terrain);
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
@@ -112,6 +131,8 @@ public final class RuntimeAgeBackend {
|
||||
String name = ageName(id);
|
||||
AgeLevelIndex index = index(server);
|
||||
long seed = requireSeed(server, id);
|
||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||
PortAgeTerrain terrain = index.terrain(id);
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
ArchivePaths paths = archivePaths(server, name);
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
@@ -130,14 +151,14 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed);
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, seed);
|
||||
load(server, id, seed, biomePlan, terrain);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
@@ -233,17 +254,49 @@ public final class RuntimeAgeBackend {
|
||||
private record ArchivePaths(Path live, Path archive) {
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed) {
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||
|
||||
// This prototype uses vanilla Overworld terrain until issue #98 ports
|
||||
// authored ChunkGenerators. LevelStem.seedOverride controls chunk random
|
||||
// state as well as ServerLevel.getSeed() in NeoForge 26.3.
|
||||
// Noise terrains reuse the Overworld biome source unless a
|
||||
// non-Native authored distribution replaces it.
|
||||
// LevelStem.seedOverride controls chunk random state and getSeed().
|
||||
LevelStem overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
|
||||
.getValue(LevelStem.OVERWORLD);
|
||||
if (overworldStem == null) throw new IllegalStateException("Missing Overworld LevelStem");
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), overworldStem.generator(), OptionalLong.of(seed));
|
||||
var generator = overworldStem.generator();
|
||||
if ((biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
|| terrain != PortAgeTerrain.STANDARD) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
||||
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
||||
var source = generator.getBiomeSource();
|
||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
List<Holder<Biome>> biomes = biomePlan.biomes().stream().map(biome ->
|
||||
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, biome))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome)))
|
||||
.toList();
|
||||
source = switch (biomePlan.distribution()) {
|
||||
case NATIVE -> throw new IllegalStateException("Native is not an override");
|
||||
case SINGLE -> new FixedBiomeSource(biomes.getFirst());
|
||||
case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
||||
biomePlan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
||||
case TINY, SMALL, MEDIUM, LARGE, HUGE -> new PortScaledBiomeSource(
|
||||
HolderSet.direct(biomes), seed, biomePlan.distribution().ordinal() -
|
||||
PortBiomePlan.Distribution.TINY.ordinal());
|
||||
};
|
||||
}
|
||||
var settings = switch (terrain) {
|
||||
case STANDARD -> noise.generatorSettings();
|
||||
case AMPLIFIED -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
||||
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
}
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.AmethystBlock;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.BuddingAmethystBlock;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** Vanilla amethyst growth, using Rivencraft's blue crystal stages. */
|
||||
public final class BuddingCrystalBlock extends AmethystBlock {
|
||||
public BuddingCrystalBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
|
||||
if (random.nextInt(5) != 0) return;
|
||||
Direction direction = Direction.values()[random.nextInt(Direction.values().length)];
|
||||
BlockPos target = pos.relative(direction);
|
||||
BlockState existing = level.getBlockState(target);
|
||||
Block next = nextStage(existing, direction);
|
||||
if (next == null) return;
|
||||
level.setBlockAndUpdate(target, next.defaultBlockState()
|
||||
.setValue(AmethystClusterBlock.FACING, direction)
|
||||
.setValue(AmethystClusterBlock.WATERLOGGED,
|
||||
existing.getFluidState().getType() == Fluids.WATER));
|
||||
}
|
||||
|
||||
public static Block nextStage(BlockState existing, Direction direction) {
|
||||
if (BuddingAmethystBlock.canClusterGrowAtState(existing)) return Rivencraft.SMALL_CRYSTAL_BUD.get();
|
||||
if (!existing.hasProperty(AmethystClusterBlock.FACING)
|
||||
|| existing.getValue(AmethystClusterBlock.FACING) != direction) return null;
|
||||
if (existing.is(Rivencraft.SMALL_CRYSTAL_BUD)) return Rivencraft.MEDIUM_CRYSTAL_BUD.get();
|
||||
if (existing.is(Rivencraft.MEDIUM_CRYSTAL_BUD)) return Rivencraft.LARGE_CRYSTAL_BUD.get();
|
||||
if (existing.is(Rivencraft.LARGE_CRYSTAL_BUD)) return Rivencraft.CRYSTAL_CLUSTER.get();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
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.ItemStack;
|
||||
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.phys.BlockHitResult;
|
||||
|
||||
/** 26.3 API adaptation of the 1.21.1 Ink Mixer block. */
|
||||
public final class InkMixerBlock extends BaseEntityBlock {
|
||||
public InkMixerBlock(Properties properties) { super(properties); }
|
||||
|
||||
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new InkMixerBlockEntity(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 InkMixerBlockEntity mixer) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new InkMixerMenu(id, inventory, mixer),
|
||||
Component.translatable("container.rivencraft.ink_mixer")),
|
||||
buffer -> buffer.writeBlockPos(pos));
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override public void playerDestroy(ServerLevel level, ServerPlayer player, BlockPos pos,
|
||||
BlockState state, BlockEntity entity, ItemStack tool) {
|
||||
if (entity instanceof InkMixerBlockEntity mixer) Containers.dropContents(level, pos, mixer);
|
||||
super.playerDestroy(level, player, pos, state, entity, tool);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
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.server.level.ServerLevel;
|
||||
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.ItemStack;
|
||||
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.EnumProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class PortBookBinderBlock extends BaseEntityBlock {
|
||||
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
|
||||
|
||||
public PortBookBinderBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@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 PortBookBinderBlockEntity(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 PortBookBinderBlockEntity binder) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new PortBookBinderMenu(id, inventory, binder),
|
||||
Component.translatable("container.rivencraft.book_binder")),
|
||||
buffer -> {
|
||||
buffer.writeBlockPos(pos);
|
||||
buffer.writeUtf(binder.title(), 32);
|
||||
});
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
@Override public void playerDestroy(ServerLevel level, ServerPlayer player, BlockPos pos,
|
||||
BlockState state, BlockEntity entity, ItemStack tool) {
|
||||
if (entity instanceof PortBookBinderBlockEntity binder) Containers.dropContents(level, pos, binder);
|
||||
super.playerDestroy(level, player, pos, state, entity, tool);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.InkVialExchange;
|
||||
import net.dnorberg.rivencraft.writing.InkModifierIngredients;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
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.item.Items;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** Server-owned Link Panel mixer with a persistent, Legacy-style additive mixture. */
|
||||
public final class InkMixerBlockEntity extends BlockEntity implements Container {
|
||||
public static final int INK = 0;
|
||||
public static final int PAPER = 1;
|
||||
public static final int BOTTLES = 2;
|
||||
public static final int SIZE = 3;
|
||||
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private int inkRemaining;
|
||||
private long nextModifierSeed = new Random().nextLong();
|
||||
private final double[] modifierChances = new double[3];
|
||||
private static final int[] FLAGS = {LinkProperties.DISARM,
|
||||
LinkProperties.MAINTAIN_MOMENTUM, LinkProperties.INTRA_LINKING};
|
||||
private static final String[] CHANCE_KEYS = {"DisarmChance", "MomentumChance", "IntraLinkingChance"};
|
||||
private static final int[] OLD_CHANCE_INDEX = {0, 2, 3};
|
||||
|
||||
public InkMixerBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.INK_MIXER_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
|
||||
public int inkRemaining() { return inkRemaining; }
|
||||
|
||||
public int modifierChancePermille(int index) {
|
||||
return Math.clamp((int) Math.round(modifierChances[index] * 1000), 0, 1000);
|
||||
}
|
||||
|
||||
/** Infuses a carried ingredient into the current ink pool; only the server calls this. */
|
||||
public boolean addModifier(ItemStack carried, int amount) {
|
||||
InkModifierIngredients.Ingredient ingredient = InkModifierIngredients.find(carried);
|
||||
if (ingredient == null || inkRemaining == 0 || amount <= 0 || carried.getCount() < amount)
|
||||
return false;
|
||||
for (InkModifierIngredients.Effect effect : ingredient.effects())
|
||||
for (int index = 0; index < FLAGS.length; index++)
|
||||
if (FLAGS[index] == effect.flag())
|
||||
modifierChances[index] = 1.0 - (1.0 - modifierChances[index])
|
||||
* Math.pow(1.0 - effect.chance(), amount);
|
||||
carried.shrink(amount);
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ItemStack previewLinkPanel() {
|
||||
if (!canCreateLinkPanel()) return ItemStack.EMPTY;
|
||||
ItemStack panel = PageItem.createLinkPanelPage();
|
||||
LinkProperties properties = rollProperties(nextModifierSeed).properties();
|
||||
if (properties.flags() != 0) panel.set(Rivencraft.LINK_PROPERTIES.get(), properties);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private record Roll(LinkProperties properties, long nextSeed) {}
|
||||
|
||||
private Roll rollProperties(long seed) {
|
||||
Random random = new Random(seed);
|
||||
int selected = 0;
|
||||
for (int index = 0; index < FLAGS.length; index++)
|
||||
if (modifierChances[index] > 0 && random.nextDouble() < modifierChances[index])
|
||||
selected |= FLAGS[index];
|
||||
return new Roll(new LinkProperties(selected), random.nextLong());
|
||||
}
|
||||
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::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();
|
||||
if (slot == BOTTLES) refillEmptyBasin();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
|
||||
@Override public void setItem(int slot, ItemStack stack) {
|
||||
items.set(slot, stack);
|
||||
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
|
||||
setChanged();
|
||||
if (slot == INK || slot == BOTTLES) refillEmptyBasin();
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
return switch (slot) {
|
||||
case INK -> stack.is(Rivencraft.INK_VIAL.get());
|
||||
case PAPER -> stack.is(Items.PAPER);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
/** No server tick: refill only when ink, bottle space, or consumption changes. */
|
||||
private void refillEmptyBasin() {
|
||||
if (level != null && !level.isClientSide() && inkRemaining == 0) {
|
||||
int addedInk = InkVialExchange.exchange(items, INK, BOTTLES);
|
||||
if (addedInk > 0) {
|
||||
inkRemaining = addedInk;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canCreateLinkPanel() {
|
||||
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
||||
return InkVialExchange.canPay(items, INK, BOTTLES, inkRemaining, 1);
|
||||
}
|
||||
|
||||
/** Commit inputs only when a virtual output is actually taken. */
|
||||
public boolean consumeLinkPanelMaterials() {
|
||||
if (!canCreateLinkPanel()) return false;
|
||||
int cost = 1;
|
||||
if (inkRemaining < cost) {
|
||||
inkRemaining += InkVialExchange.exchange(items, INK, BOTTLES);
|
||||
}
|
||||
inkRemaining -= cost;
|
||||
items.get(PAPER).shrink(1);
|
||||
if (items.get(PAPER).isEmpty()) items.set(PAPER, ItemStack.EMPTY);
|
||||
nextModifierSeed = rollProperties(nextModifierSeed).nextSeed();
|
||||
java.util.Arrays.fill(modifierChances, 0);
|
||||
setChanged();
|
||||
if (inkRemaining == 0) refillEmptyBasin();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public void setLevel(Level level) {
|
||||
super.setLevel(level);
|
||||
refillEmptyBasin();
|
||||
}
|
||||
@Override public void clearContent() {
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
inkRemaining = 0;
|
||||
java.util.Arrays.fill(modifierChances, 0);
|
||||
setChanged();
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
inkRemaining = Math.clamp(input.getIntOr("InkRemaining", 0), 0, InkVialItem.CAPACITY);
|
||||
nextModifierSeed = input.getLongOr("NextModifierSeed", nextModifierSeed);
|
||||
for (int index = 0; index < modifierChances.length; index++) {
|
||||
modifierChances[index] = Math.clamp(input.getDoubleOr(CHANCE_KEYS[index],
|
||||
input.getDoubleOr("ModifierChance" + OLD_CHANCE_INDEX[index], 0.0)), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
output.putInt("InkRemaining", inkRemaining);
|
||||
output.putLong("NextModifierSeed", nextModifierSeed);
|
||||
for (int index = 0; index < modifierChances.length; index++)
|
||||
output.putDouble(CHANCE_KEYS[index], modifierChances[index]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
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.item.Items;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** Persistent, server-owned Binder inputs. */
|
||||
public final class PortBookBinderBlockEntity extends BlockEntity implements Container {
|
||||
public static final int COVER = 0;
|
||||
public static final int PANEL = 1;
|
||||
public static final int OUTPUT = 2;
|
||||
public static final int FIRST_PAGE = 3;
|
||||
public static final int PAGE_COUNT = 252;
|
||||
public static final int SIZE = FIRST_PAGE + PAGE_COUNT;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private String title = "";
|
||||
|
||||
public PortBookBinderBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.BOOK_BINDER_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
|
||||
public String title() { return title; }
|
||||
|
||||
public void setTitle(String value) {
|
||||
String normalized = value == null ? "" : value.strip();
|
||||
if (normalized.length() > 32) return;
|
||||
if (!title.equals(normalized)) {
|
||||
title = normalized;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
|
||||
@Override public void clearContent() {
|
||||
items.clear();
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
}
|
||||
@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);
|
||||
setChanged();
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
if (slot == COVER) return stack.is(Items.LEATHER);
|
||||
if (slot == PANEL) return PageItem.isLinkPanel(stack);
|
||||
return slot >= FIRST_PAGE && slot < SIZE && validSymbolPage(stack);
|
||||
}
|
||||
private static boolean validSymbolPage(ItemStack stack) {
|
||||
return stack.is(Rivencraft.PAGE.get()) && !PageItem.isLinkPanel(stack)
|
||||
&& stack.has(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
}
|
||||
|
||||
public boolean hasSymbolPages() {
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++)
|
||||
if (!items.get(slot).isEmpty()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<Identifier> symbolPages() {
|
||||
List<Identifier> pages = new ArrayList<>();
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) {
|
||||
Identifier symbol = items.get(slot).get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbol != null) pages.add(symbol);
|
||||
}
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
public void clearSymbolPages() {
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) items.set(slot, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public int nextPageIndex() {
|
||||
for (int index = PAGE_COUNT - 1; index >= 0; index--)
|
||||
if (!items.get(FIRST_PAGE + index).isEmpty()) return index + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean insertPage(int index, ItemStack page) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index > end || end >= PAGE_COUNT || page.getCount() != 1
|
||||
|| !validSymbolPage(page)) return false;
|
||||
for (int current = end; current > index; current--)
|
||||
items.set(FIRST_PAGE + current, items.get(FIRST_PAGE + current - 1));
|
||||
items.set(FIRST_PAGE + index, page.copy());
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ItemStack takePage(int index) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index >= end) return ItemStack.EMPTY;
|
||||
ItemStack removed = items.get(FIRST_PAGE + index);
|
||||
for (int current = index; current < end - 1; current++)
|
||||
items.set(FIRST_PAGE + current, items.get(FIRST_PAGE + current + 1));
|
||||
items.set(FIRST_PAGE + end - 1, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return level != null && level.getBlockEntity(worldPosition) == this
|
||||
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
|
||||
worldPosition.getZ() + 0.5) <= 64.0;
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
output.putString("Title", title);
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
title = input.getStringOr("Title", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** 1.21.1 Legacy atlas and mixer animation adapted to the 26.3 GUI API. */
|
||||
public final class InkMixerScreen extends AbstractContainerScreen<InkMixerMenu> {
|
||||
private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "textures/gui/ink_mixer_legacy.png");
|
||||
private static final int BASIN_X = 88;
|
||||
private static final int BASIN_Y = 49;
|
||||
private static final int BASIN_RADIUS = 30;
|
||||
private static final int[] PROPERTY_COLORS = {0xFFE84848, 0xFF5555FF, 0xFF55EF55};
|
||||
private final int[] lastChances = new int[3];
|
||||
private boolean basinClickPendingRelease;
|
||||
private long lastAdditionTime;
|
||||
|
||||
public InkMixerScreen(InkMixerMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 181);
|
||||
inventoryLabelY = 87;
|
||||
}
|
||||
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
@Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractBackground(graphics, mouseX, mouseY, partialTick);
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left + 54, top + 16,
|
||||
179, 16, 66, 65, 256, 256);
|
||||
if (menu.inkRemaining() > 0) {
|
||||
graphics.fill(left + 54, top + 16, left + 120, top + 81, 0xFF101014);
|
||||
renderPropertyGlyph(graphics);
|
||||
}
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top,
|
||||
0, 0, imageWidth, imageHeight, 256, 256);
|
||||
}
|
||||
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false);
|
||||
}
|
||||
|
||||
@Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
super.extractTooltip(graphics, mouseX, mouseY);
|
||||
if (overBasin(mouseX, mouseY)) {
|
||||
var lines = new java.util.ArrayList<Component>();
|
||||
lines.add(Component.translatable("screen.rivencraft.ink_mixer.ink",
|
||||
menu.inkRemaining(), InkVialItem.CAPACITY));
|
||||
lines.add(Component.translatable("screen.rivencraft.ink_mixer.additive_hint"));
|
||||
for (int index = 0; index < 3; index++) {
|
||||
int chance = menu.modifierChancePermille(index);
|
||||
if (chance > 0) lines.add(Component.translatable("screen.rivencraft.ink_mixer.chance",
|
||||
switch (index) {
|
||||
case 0 -> Component.translatable("item.rivencraft.link_property.disarm");
|
||||
case 1 -> Component.translatable("item.rivencraft.link_property.maintain_momentum");
|
||||
default -> Component.translatable("item.rivencraft.link_property.intra_linking");
|
||||
}, chance / 10.0));
|
||||
}
|
||||
graphics.setComponentTooltipForNextFrame(font, lines, mouseX, mouseY, ItemStack.EMPTY, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
|| event.button() == InputConstants.MOUSE_BUTTON_RIGHT)
|
||||
&& overBasin(event.x(), event.y()) && !menu.getCarried().isEmpty()) {
|
||||
minecraft.gameMode.handleContainerInput(menu.containerId, InkMixerMenu.MODIFIER,
|
||||
event.button() == InputConstants.MOUSE_BUTTON_RIGHT ? 1 : 0,
|
||||
ContainerInput.PICKUP, minecraft.player);
|
||||
basinClickPendingRelease = true;
|
||||
return true;
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override public boolean mouseReleased(MouseButtonEvent event) {
|
||||
if (basinClickPendingRelease) {
|
||||
basinClickPendingRelease = false;
|
||||
return true;
|
||||
}
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
private boolean overBasin(double mouseX, double mouseY) {
|
||||
double x = mouseX - leftPos - BASIN_X;
|
||||
double y = mouseY - topPos - BASIN_Y;
|
||||
return x * x + y * y < BASIN_RADIUS * BASIN_RADIUS;
|
||||
}
|
||||
|
||||
/** A GUI-scale projection of Legacy's colored, rotating D'ni ink effect. */
|
||||
private void renderPropertyGlyph(GuiGraphicsExtractor graphics) {
|
||||
long now = System.currentTimeMillis();
|
||||
int[] active = new int[3];
|
||||
int count = 0;
|
||||
int strongest = 0;
|
||||
for (int index = 0; index < lastChances.length; index++) {
|
||||
int chance = menu.modifierChancePermille(index);
|
||||
if (chance > lastChances[index]) lastAdditionTime = now;
|
||||
lastChances[index] = chance;
|
||||
if (chance > 0) {
|
||||
active[count++] = index;
|
||||
strongest = Math.max(strongest, chance);
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
lastAdditionTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double cycle = now / 1800.0;
|
||||
int first = active[(int) cycle % count];
|
||||
int second = active[((int) cycle + 1) % count];
|
||||
double blend = (1.0 - Math.cos((cycle % 1.0) * Math.PI)) / 2.0;
|
||||
int rgb = blendRgb(PROPERTY_COLORS[first], PROPERTY_COLORS[second], blend);
|
||||
double pulse = Math.max(0.0, 1.0 - (now - lastAdditionTime) / 700.0);
|
||||
int alpha = Math.clamp((int) (70 + strongest * 0.14 + pulse * 45), 0, 255);
|
||||
int color = alpha << 24 | rgb;
|
||||
int centerX = leftPos + BASIN_X;
|
||||
int centerY = topPos + BASIN_Y;
|
||||
double turn = now / 1200.0;
|
||||
|
||||
for (int spoke = 0; spoke < 4; spoke++) {
|
||||
for (int step = 0; step <= 30; step++) {
|
||||
double progress = step / 30.0;
|
||||
double angle = turn + spoke * Math.PI / 2.0 + progress * 0.9;
|
||||
double radius = 3.0 + progress * (15.0 + pulse * 3.0);
|
||||
plot(graphics, centerX + Math.cos(angle) * radius,
|
||||
centerY + Math.sin(angle) * radius, color);
|
||||
}
|
||||
}
|
||||
for (int step = 0; step < 96; step++) {
|
||||
double angle = step * Math.PI / 48.0 + turn * 0.3;
|
||||
plot(graphics, centerX + Math.cos(angle) * 19.0,
|
||||
centerY + Math.sin(angle) * 19.0, color);
|
||||
if (strongest >= 990 && step % 2 == 0) {
|
||||
plot(graphics, centerX + Math.cos(angle) * 23.0,
|
||||
centerY + Math.sin(angle) * 23.0, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int blendRgb(int first, int second, double amount) {
|
||||
int red = (int) (((first >> 16) & 0xFF) * (1.0 - amount)
|
||||
+ ((second >> 16) & 0xFF) * amount);
|
||||
int green = (int) (((first >> 8) & 0xFF) * (1.0 - amount)
|
||||
+ ((second >> 8) & 0xFF) * amount);
|
||||
int blue = (int) ((first & 0xFF) * (1.0 - amount) + (second & 0xFF) * amount);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
private static void plot(GuiGraphicsExtractor graphics, double x, double y, int color) {
|
||||
int pixelX = (int) Math.round(x);
|
||||
int pixelY = (int) Math.round(y);
|
||||
graphics.fill(pixelX, pixelY, pixelX + 1, pixelY + 1, color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
||||
import net.neoforged.neoforge.client.event.RegisterSpecialModelRendererEvent;
|
||||
import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortBookBinderClientEvents {
|
||||
private PortBookBinderClientEvents() {}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
||||
event.register(Rivencraft.INK_MIXER_MENU.get(), InkMixerScreen::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerPageRenderer(RegisterSpecialModelRendererEvent event) {
|
||||
event.register(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "symbol_page"),
|
||||
PortSymbolPageRenderer.Unbaked.MAP_CODEC);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerPageTextureReload(AddClientReloadListenersEvent event) {
|
||||
event.addListener(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "symbol_page_textures"),
|
||||
(ResourceManagerReloadListener) manager -> PortSymbolPageTextures.clear());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.network.AssembleBinderPayload;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.SetBinderTitlePayload;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Legacy-sized Binder with a page strip and single-click book assembly. */
|
||||
public final class PortBookBinderScreen extends AbstractContainerScreen<PortBookBinderMenu> {
|
||||
private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(
|
||||
"rivencraft", "textures/gui/book_binder_legacy.png");
|
||||
private static final Identifier PAGE_ATLAS = Identifier.fromNamespaceAndPath(
|
||||
"rivencraft", "textures/gui/notebook_pages.png");
|
||||
private static final int PAGE_Y = 48;
|
||||
private static final int PAGE_WIDTH = 26;
|
||||
private static final int PAGE_HEIGHT = 34;
|
||||
private static final int PAGE_STEP = 28;
|
||||
private static final int VISIBLE_PAGES = 5;
|
||||
private static final int STRIP_X = 7;
|
||||
private static final int STRIP_Y = 45;
|
||||
private static final int STRIP_WIDTH = 162;
|
||||
private static final int STRIP_HEIGHT = 40;
|
||||
private static final int MISSING_PANEL_X = 27;
|
||||
private static final int MISSING_PANEL_Y = 26;
|
||||
private static final int MISSING_PANEL_SIZE = 16;
|
||||
private static final ItemStack MISSING_PANEL_ICON = PageItem.createLinkPanelPage();
|
||||
private EditBox titleInput;
|
||||
private boolean stripClickPendingRelease;
|
||||
private int firstVisible;
|
||||
|
||||
public PortBookBinderScreen(PortBookBinderMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 181);
|
||||
inventoryLabelY = 87;
|
||||
}
|
||||
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
@Override protected void init() {
|
||||
super.init();
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
titleInput = new EditBox(font, left + 9, top + 10, 112, 12, Component.literal("Age name"));
|
||||
titleInput.setBordered(false);
|
||||
titleInput.setTextColor(0xFFFFFFFF);
|
||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setValue(menu.title());
|
||||
titleInput.setHint(Component.literal("Name this Age"));
|
||||
titleInput.setResponder(value -> ClientPacketDistributor.sendToServer(
|
||||
new SetBinderTitlePayload(menu.containerId, value)));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
}
|
||||
|
||||
@Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractBackground(graphics, mouseX, mouseY, partialTick);
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, 0, 0,
|
||||
imageWidth, imageHeight, 256, 256);
|
||||
graphics.fill(left + 7, top + 9, left + 123, top + 23, 0xFF000000);
|
||||
firstVisible = Math.min(firstVisible, maxFirstVisible());
|
||||
int visibleCount = Math.min(VISIBLE_PAGES, visiblePageCount() - firstVisible);
|
||||
for (int visible = 0; visible < visibleCount; visible++) {
|
||||
int x = left + 16 + visible * PAGE_STEP;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, PAGE_ATLAS,
|
||||
x, top + PAGE_Y, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
|
||||
}
|
||||
// GUI extraction batches textures within a stratum. The glyphs need a
|
||||
// later stratum so another atlas page cannot cover their transparent quad.
|
||||
graphics.nextStratum();
|
||||
for (int visible = 0; visible < visibleCount; visible++) {
|
||||
int index = firstVisible + visible;
|
||||
int x = left + 16 + visible * PAGE_STEP;
|
||||
int y = top + PAGE_Y;
|
||||
ItemStack page = pageAt(index);
|
||||
if (page.isEmpty()) {
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xAA000000);
|
||||
} else if (PageItem.isLinkPanel(page)) {
|
||||
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
||||
} else {
|
||||
// Render the full-size transparent glyph through the working
|
||||
// item pipeline, without its small parchment-backed icon.
|
||||
ItemStack glyphOnly = page.copy();
|
||||
glyphOnly.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x + 1, y + 5);
|
||||
graphics.pose().scale(1.5F, 1.5F);
|
||||
graphics.fakeItem(glyphOnly, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
}
|
||||
}
|
||||
graphics.fill(left + STRIP_X, top + STRIP_Y, left + STRIP_X + 8,
|
||||
top + STRIP_Y + STRIP_HEIGHT, firstVisible == 0 ? 0x33000000 : 0xAA000000);
|
||||
graphics.fill(left + STRIP_X + STRIP_WIDTH - 8, top + STRIP_Y,
|
||||
left + STRIP_X + STRIP_WIDTH, top + STRIP_Y + STRIP_HEIGHT,
|
||||
firstVisible >= maxFirstVisible() ? 0x33000000 : 0xAA000000);
|
||||
boolean panelPresent = !menu.getSlot(PortBookBinderBlockEntity.PANEL).getItem().isEmpty();
|
||||
if (titleInput == null || titleInput.getValue().isBlank()) {
|
||||
graphics.fill(left + 7, top + 9, left + 123, top + 10, 0xFFFF3030);
|
||||
graphics.fill(left + 7, top + 22, left + 123, top + 23, 0xFFFF3030);
|
||||
}
|
||||
if (!panelPresent) {
|
||||
graphics.fakeItem(MISSING_PANEL_ICON, left + MISSING_PANEL_X, top + MISSING_PANEL_Y);
|
||||
float phase = (System.currentTimeMillis() % 4000L) / 2000.0F;
|
||||
float fade = phase > 1.0F ? 2.0F - phase : phase;
|
||||
int alpha = 40 + (int) (125 * fade);
|
||||
graphics.fill(left + MISSING_PANEL_X, top + MISSING_PANEL_Y,
|
||||
left + MISSING_PANEL_X + MISSING_PANEL_SIZE,
|
||||
top + MISSING_PANEL_Y + MISSING_PANEL_SIZE,
|
||||
(alpha << 24) | 0x00B02020);
|
||||
}
|
||||
if (hasUnsupportedPages()) graphics.text(font,
|
||||
Component.translatable("screen.rivencraft.book_binder.authored_pending"),
|
||||
left + 75, top + 31, 0xFFB84B42, false);
|
||||
if (!hasUnsupportedPages() && menu.getSlot(2).getItem().isEmpty() && !menu.getSlot(0).getItem().isEmpty()
|
||||
&& !menu.getSlot(1).getItem().isEmpty() && titleInput != null
|
||||
&& !titleInput.getValue().isBlank()) {
|
||||
graphics.fakeItem(new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()), left + 152, top + 27);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false);
|
||||
}
|
||||
|
||||
@Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
super.extractTooltip(graphics, mouseX, mouseY);
|
||||
int x = mouseX - leftPos;
|
||||
int y = mouseY - topPos;
|
||||
if (x >= MISSING_PANEL_X && x < MISSING_PANEL_X + MISSING_PANEL_SIZE
|
||||
&& y >= MISSING_PANEL_Y && y < MISSING_PANEL_Y + MISSING_PANEL_SIZE
|
||||
&& menu.getSlot(1).getItem().isEmpty()) {
|
||||
graphics.setTooltipForNextFrame(font,
|
||||
Component.translatable("screen.rivencraft.book_binder.missing_panel"), mouseX, mouseY);
|
||||
} else {
|
||||
int index = pageIndexAt(mouseX, mouseY);
|
||||
if (index >= 0 && !pageAt(index).isEmpty())
|
||||
graphics.setTooltipForNextFrame(font, pageAt(index), mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack pageAt(int index) {
|
||||
return menu.getSlot(index == 0 ? PortBookBinderBlockEntity.PANEL
|
||||
: PortBookBinderBlockEntity.FIRST_PAGE + index - 1).getItem();
|
||||
}
|
||||
|
||||
private int displayedPages() {
|
||||
for (int index = PortBookBinderBlockEntity.PAGE_COUNT; index >= 1; index--)
|
||||
if (!pageAt(index).isEmpty()) return index + 1;
|
||||
return pageAt(0).isEmpty() ? 0 : 1;
|
||||
}
|
||||
|
||||
private boolean hasUnsupportedPages() {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int index = 1; index <= PortBookBinderBlockEntity.PAGE_COUNT; index++) {
|
||||
Identifier id = pageAt(index).get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (id != null) pages.add(id);
|
||||
}
|
||||
return !PortAuthoredWriting.supports(pages);
|
||||
}
|
||||
|
||||
private int visiblePageCount() {
|
||||
return Math.min(PortBookBinderBlockEntity.PAGE_COUNT + 1,
|
||||
displayedPages() + (menu.getCarried().isEmpty() ? 0 : 1));
|
||||
}
|
||||
|
||||
private int maxFirstVisible() { return Math.max(0, visiblePageCount() - VISIBLE_PAGES); }
|
||||
|
||||
private int pageIndexAt(double mouseX, double mouseY) {
|
||||
int x = (int) mouseX - leftPos;
|
||||
int y = (int) mouseY - topPos;
|
||||
if (x < 16 || x >= STRIP_X + STRIP_WIDTH - 8 || y < STRIP_Y || y >= STRIP_Y + STRIP_HEIGHT)
|
||||
return -1;
|
||||
int visible = (x - 16) / PAGE_STEP;
|
||||
return visible < VISIBLE_PAGES ? firstVisible + visible : -1;
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
|| event.button() == InputConstants.MOUSE_BUTTON_RIGHT)
|
||||
&& event.x() >= left + STRIP_X && event.x() < left + STRIP_X + STRIP_WIDTH
|
||||
&& event.y() >= top + STRIP_Y && event.y() < top + STRIP_Y + STRIP_HEIGHT) {
|
||||
stripClickPendingRelease = true;
|
||||
if (event.x() < left + STRIP_X + 8) firstVisible = Math.max(0, firstVisible - 1);
|
||||
else if (event.x() >= left + STRIP_X + STRIP_WIDTH - 8)
|
||||
firstVisible = Math.min(maxFirstVisible(), firstVisible + 1);
|
||||
else {
|
||||
int index = pageIndexAt(event.x(), event.y());
|
||||
if (index >= 0 && index <= displayedPages()
|
||||
&& (index < displayedPages() || !menu.getCarried().isEmpty()))
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
PortBookBinderMenu.STRIP_CLICK_BASE + index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
&& event.x() >= left + 151 && event.x() < left + 170
|
||||
&& event.y() >= top + 26 && event.y() < top + 45
|
||||
&& !hasUnsupportedPages() && menu.getSlot(2).getItem().isEmpty()
|
||||
&& menu.getCarried().isEmpty()) {
|
||||
String name = titleInput.getValue().strip();
|
||||
if (!name.isEmpty()) ClientPacketDistributor.sendToServer(new AssembleBinderPayload(name));
|
||||
return true;
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override public boolean mouseReleased(MouseButtonEvent event) {
|
||||
if (stripClickPendingRelease) {
|
||||
stripClickPendingRelease = false;
|
||||
return true;
|
||||
}
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (mouseX >= leftPos + STRIP_X && mouseX < leftPos + STRIP_X + STRIP_WIDTH
|
||||
&& mouseY >= topPos + STRIP_Y && mouseY < topPos + STRIP_Y + STRIP_HEIGHT) {
|
||||
firstVisible = Math.clamp(firstVisible - (int) Math.signum(scrollY), 0, maxFirstVisible());
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(KeyEvent event) {
|
||||
if (titleInput != null && titleInput.isFocused()) {
|
||||
if (event.isEscape()) {
|
||||
setFocused(null);
|
||||
titleInput.setFocused(false);
|
||||
} else {
|
||||
titleInput.keyPressed(event);
|
||||
}
|
||||
// The character event inserts text. Never forward a focused key
|
||||
// to the container's inventory/hotbar shortcuts.
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/** Flat item geometry textured with a page composed from its symbol stack. */
|
||||
public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSymbolPageRenderer.PageArt> {
|
||||
@Override
|
||||
public PageArt extractArgument(ItemStack stack) {
|
||||
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
return symbol == null ? null : new PageArt(symbol,
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PageArt art, PoseStack pose, SubmitNodeCollector collector,
|
||||
int light, int overlay, boolean hasFoil, int outlineColor) {
|
||||
if (art == null) return;
|
||||
Identifier texture = art.glyphOnly()
|
||||
? PortSymbolPageTextures.forPage(art.symbol())
|
||||
: PortSymbolPageTextures.forItem(art.symbol());
|
||||
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
||||
(matrix, vertices) -> {
|
||||
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay);
|
||||
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay);
|
||||
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay);
|
||||
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getExtents(Consumer<org.joml.Vector3fc> extents) {
|
||||
extents.accept(new Vector3f(0, 0, 0.5F));
|
||||
extents.accept(new Vector3f(1, 1, 0.5F));
|
||||
}
|
||||
|
||||
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
|
||||
float x, float y, float u, float v, int light, int overlay) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(255, 255, 255, 255)
|
||||
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
|
||||
}
|
||||
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly) {}
|
||||
|
||||
public record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
|
||||
public static final Unbaked INSTANCE = new Unbaked();
|
||||
public static final MapCodec<Unbaked> MAP_CODEC = MapCodec.unit(INSTANCE);
|
||||
|
||||
@Override public MapCodec<Unbaked> type() { return MAP_CODEC; }
|
||||
@Override public SpecialModelRenderer<PageArt> bake(BakingContext context) {
|
||||
return new PortSymbolPageRenderer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolNumerals;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.ARGB;
|
||||
|
||||
/** The 1.21.1 Legacy-style glyph compositor adapted to 26.3's texture API. */
|
||||
public final class PortSymbolPageTextures {
|
||||
private static final int SIZE = 128;
|
||||
private static final int GLYPH = 51;
|
||||
private static final int ITEM_SYMBOL_SIZE = 15;
|
||||
private static final int NUMBER_GLYPH_SIZE = 64;
|
||||
private static final int CACHE_LIMIT = 256;
|
||||
private static final Identifier PAGE = texture("item/page");
|
||||
private static final Identifier COMPONENTS = texture("gui/symbolcomponents");
|
||||
private static final Identifier FALLBACK = texture("item/page_symbol");
|
||||
private static final Map<String, Identifier> CACHE = new LinkedHashMap<>(16, .75F, true);
|
||||
private static final Map<String, int[]> WORD_CACHE = new LinkedHashMap<>(16, .75F, true);
|
||||
private static NativeImage page;
|
||||
private static NativeImage components;
|
||||
private static int nextId;
|
||||
|
||||
// WordData from Mystcraft Legacy 0.13.7.06, matching the 1.21.1 compositor.
|
||||
private static final Map<String, int[]> WORDS = Map.ofEntries(
|
||||
Map.entry("Terrain", new int[] {6,10,12,13,16,19,24,25,27}),
|
||||
Map.entry("Form", new int[] {4,6,8,9,10,11,13,14,16,23}),
|
||||
Map.entry("Tradition", new int[] {4,6,7,8,9,14,17,18,20}),
|
||||
Map.entry("Flow", new int[] {4,7,12,13,17,18}),
|
||||
Map.entry("Constraint", new int[] {5,6,9,11,13,20,22,23}),
|
||||
Map.entry("Nature", new int[] {5,6,8,10,11,12,15,16,17,22}),
|
||||
Map.entry("Sustain", new int[] {6,16,19,20,21,23}),
|
||||
Map.entry("Ethereal", new int[] {4,6,8,13,14,16,18,21,23}),
|
||||
Map.entry("Dynamic", new int[] {4,5,6,12,13,17,18,22,23}),
|
||||
Map.entry("Cycle", new int[] {4,5,7,16,17,18}),
|
||||
Map.entry("Balance", new int[] {4,6,9,10,16,19}),
|
||||
Map.entry("Transform", new int[] {4,5,6,8,11,14,18,20,21}),
|
||||
Map.entry("Time", new int[] {4,5,7,12,13,19,20,21,22,23}),
|
||||
Map.entry("System", new int[] {10,12,14,15,16,20,21,22}),
|
||||
Map.entry("Void", new int[] {4,5,6,16,17,18,23})
|
||||
);
|
||||
|
||||
private PortSymbolPageTextures() {}
|
||||
|
||||
public static Identifier forItem(Identifier id) {
|
||||
return forSymbol(id, true);
|
||||
}
|
||||
|
||||
/** Transparent, full-size glyphs layered over the Binder's page atlas. */
|
||||
public static Identifier forPage(Identifier id) {
|
||||
return forSymbol(id, false);
|
||||
}
|
||||
|
||||
private static Identifier forSymbol(Identifier id, boolean item) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(id);
|
||||
if (entry == null) return FALLBACK;
|
||||
String poem = entry.poem();
|
||||
if (!poem.matches("[A-Za-z]+(?: [A-Za-z]+){3}")) poem = "";
|
||||
String key = id + "|" + poem + (item ? "|item" : "|page");
|
||||
Identifier cached = CACHE.get(key);
|
||||
if (cached != null) return cached;
|
||||
try {
|
||||
loadSources(Minecraft.getInstance().getResourceManager());
|
||||
NativeImage image = compose(id, poem, item);
|
||||
Identifier location = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"generated/page_" + nextId++);
|
||||
Minecraft.getInstance().getTextureManager().register(location,
|
||||
new DynamicTexture(() -> "Rivencraft symbol page " + id, image));
|
||||
CACHE.put(key, location);
|
||||
if (CACHE.size() > CACHE_LIMIT) {
|
||||
String eldest = CACHE.keySet().iterator().next();
|
||||
Minecraft.getInstance().getTextureManager().release(CACHE.remove(eldest));
|
||||
}
|
||||
return location;
|
||||
} catch (IOException exception) {
|
||||
Rivencraft.LOGGER.error("Could not compose symbol page {}", id, exception);
|
||||
return FALLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
var textures = Minecraft.getInstance().getTextureManager();
|
||||
CACHE.values().forEach(textures::release);
|
||||
CACHE.clear();
|
||||
WORD_CACHE.clear();
|
||||
if (page != null) page.close();
|
||||
if (components != null) components.close();
|
||||
page = null;
|
||||
components = null;
|
||||
}
|
||||
|
||||
private static void loadSources(ResourceManager manager) throws IOException {
|
||||
if (page != null && components != null) return;
|
||||
try (InputStream pageStream = manager.getResourceOrThrow(PAGE).open();
|
||||
InputStream componentsStream = manager.getResourceOrThrow(COMPONENTS).open()) {
|
||||
page = NativeImage.read(pageStream);
|
||||
components = NativeImage.read(componentsStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeImage compose(Identifier id, String poem, boolean item) {
|
||||
NativeImage image = new NativeImage(SIZE, SIZE, true);
|
||||
if (item) {
|
||||
for (int y = 0; y < SIZE; y++) for (int x = 0; x < SIZE; x++) {
|
||||
image.setPixel(x, y, page.getPixel(x * page.getWidth() / SIZE,
|
||||
y * page.getHeight() / SIZE));
|
||||
}
|
||||
}
|
||||
String numberPath = id.getPath().startsWith("number/")
|
||||
? id.getPath().substring("number/".length()) : "";
|
||||
if (id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& numberPath.matches("(?:[0-9]|1[0-9]|2[0-4])")) {
|
||||
int digit = Integer.parseInt(numberPath);
|
||||
int glyphSize = item ? NUMBER_GLYPH_SIZE : GLYPH;
|
||||
int origin = item ? (SIZE - glyphSize) / 2 : 38;
|
||||
for (int component : PortSymbolNumerals.componentsForDigit(digit))
|
||||
blendComponent(image, component, origin, origin, glyphSize);
|
||||
return image;
|
||||
}
|
||||
String[] words = poem.isBlank() ? generatedWords(id) : poem.split(" ");
|
||||
int[] offsetsX = {38, 77, 38, 0};
|
||||
int[] offsetsY = {0, 38, 77, 38};
|
||||
int glyphSize = item ? GLYPH * ITEM_SYMBOL_SIZE / 16 : GLYPH;
|
||||
for (int word = 0; word < 4; word++) {
|
||||
int x = item ? scaleForItem(offsetsX[word]) : offsetsX[word];
|
||||
int y = item ? scaleForItem(offsetsY[word]) : offsetsY[word];
|
||||
for (int component : glyphs(words[word]))
|
||||
blendComponent(image, component, x, y, glyphSize);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static void blendComponent(NativeImage image, int component, int x0, int y0, int size) {
|
||||
int cellX = (component % 8) * 64;
|
||||
int cellY = (component / 8) * 64;
|
||||
for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) {
|
||||
int sourceX = cellX + x * 64 / size;
|
||||
int sourceY = cellY + y * 64 / size;
|
||||
// One source-pixel of coverage keeps the thin Legacy strokes legible
|
||||
// when Minecraft shrinks this 128px composite to a 16px item icon.
|
||||
int alpha = ARGB.alpha(components.getPixel(sourceX, sourceY));
|
||||
if (sourceX > cellX) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX - 1, sourceY)));
|
||||
if (sourceX < cellX + 63) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX + 1, sourceY)));
|
||||
if (sourceY > cellY) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX, sourceY - 1)));
|
||||
if (sourceY < cellY + 63) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX, sourceY + 1)));
|
||||
if (alpha != 0) image.setPixel(x0 + x, y0 + y,
|
||||
ARGB.alphaBlend(image.getPixel(x0 + x, y0 + y),
|
||||
(alpha << 24) | 0x001A212B));
|
||||
}
|
||||
}
|
||||
|
||||
private static int scaleForItem(int coordinate) {
|
||||
return SIZE / 2 + Math.round((coordinate - SIZE / 2F) * ITEM_SYMBOL_SIZE / 16F);
|
||||
}
|
||||
|
||||
private static String[] generatedWords(Identifier id) {
|
||||
String stem = id.toString();
|
||||
return new String[] {stem + 'a', stem + 'b', stem + 'c', stem + 'd'};
|
||||
}
|
||||
|
||||
private static int[] glyphs(String word) {
|
||||
int[] known = WORDS.get(word);
|
||||
if (known != null) return known;
|
||||
String key = word.toLowerCase(Locale.ROOT);
|
||||
int[] cached = WORD_CACHE.get(key);
|
||||
if (cached != null) return cached;
|
||||
Random random = new Random(key.hashCode());
|
||||
int[] result = new int[random.nextInt(10) + 3];
|
||||
for (int i = 0; i < result.length; i++) result[i] = random.nextInt(20) + 4;
|
||||
WORD_CACHE.put(key, result);
|
||||
if (WORD_CACHE.size() > CACHE_LIMIT) WORD_CACHE.remove(WORD_CACHE.keySet().iterator().next());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Identifier texture(String path) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** A stackable 16-use vial; using it with paper in the other hand prepares a Link Panel. */
|
||||
public final class InkVialItem extends Item {
|
||||
public static final int CAPACITY = 16;
|
||||
// Match the 1.21.1 default; port the configurable cost with the full configuration system.
|
||||
public static final int LINK_PANEL_COST = 2;
|
||||
|
||||
public InkVialItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static int remaining(ItemStack vial) {
|
||||
return vial.getOrDefault(Rivencraft.INK_REMAINING.get(), CAPACITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
InteractionHand paperHand = hand == InteractionHand.MAIN_HAND
|
||||
? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
|
||||
ItemStack vial = player.getItemInHand(hand);
|
||||
ItemStack paper = player.getItemInHand(paperHand);
|
||||
int ink = remaining(vial);
|
||||
if (!vial.is(Rivencraft.INK_VIAL.get()) || !paper.is(Items.PAPER)
|
||||
|| ink < LINK_PANEL_COST || ink > CAPACITY) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
|
||||
ItemStack panel = PageItem.createLinkPanelPage();
|
||||
int newInk = ink - LINK_PANEL_COST;
|
||||
ItemStack spentVial = newInk == 0
|
||||
? new ItemStack(Items.GLASS_BOTTLE)
|
||||
: new ItemStack(Rivencraft.INK_VIAL.get());
|
||||
if (newInk > 0) spentVial.set(Rivencraft.INK_REMAINING.get(), newInk);
|
||||
|
||||
if (paper.getCount() == 1) {
|
||||
player.setItemInHand(paperHand, panel);
|
||||
} else {
|
||||
paper.shrink(1);
|
||||
giveOrDrop(player, panel);
|
||||
}
|
||||
if (vial.getCount() == 1) {
|
||||
player.setItemInHand(hand, spentVial);
|
||||
} else {
|
||||
vial.shrink(1);
|
||||
giveOrDrop(player, spentVial);
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("item.rivencraft.ink_vial.remaining", remaining(stack), CAPACITY));
|
||||
tooltip.accept(Component.translatable("item.rivencraft.ink_vial.usage"));
|
||||
}
|
||||
|
||||
private static void giveOrDrop(Player player, ItemStack stack) {
|
||||
if (!player.getInventory().add(stack)) player.drop(stack, false, Prediction.SERVER_ONLY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** The three Ink Mixer modifiers currently supported by the 26.3 port. */
|
||||
public record LinkProperties(int flags) {
|
||||
public static final int DISARM = 1;
|
||||
public static final int MAINTAIN_MOMENTUM = 4;
|
||||
public static final int INTRA_LINKING = 8;
|
||||
public static final int SUPPORTED = DISARM | MAINTAIN_MOMENTUM | INTRA_LINKING;
|
||||
public static final LinkProperties NONE = new LinkProperties(0);
|
||||
public static final Codec<LinkProperties> CODEC = Codec.INT.xmap(LinkProperties::new, LinkProperties::flags);
|
||||
public static final StreamCodec<ByteBuf, LinkProperties> STREAM_CODEC = StreamCodec.of(
|
||||
(buffer, value) -> ByteBufCodecs.VAR_INT.encode(buffer, value.flags()),
|
||||
buffer -> new LinkProperties(ByteBufCodecs.VAR_INT.decode(buffer)));
|
||||
|
||||
public LinkProperties {
|
||||
if (flags < 0 || (flags & ~SUPPORTED) != 0) {
|
||||
throw new IllegalArgumentException("Unsupported Link Panel flags: " + flags);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean has(int flag) {
|
||||
return (flags & flag) != 0;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Handheld Linking Book; destination binding and travel are server-authoritative packets. */
|
||||
public final class LinkingBookItem extends Item {
|
||||
@@ -37,10 +39,13 @@ public final class LinkingBookItem extends Item {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
public static String travel(ServerPlayer player, LinkBookTarget destination) {
|
||||
public static String travel(ServerPlayer player, ItemStack book) {
|
||||
LinkBookTarget destination = book.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
LinkProperties properties = book.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (destination == null) return "Unbound Linking Book (26.3 port preview)";
|
||||
if (!destination.valid()) return "Invalid Linking Book destination";
|
||||
if (player.level().dimension().identifier().equals(destination.dimension())) {
|
||||
if (player.level().dimension().identifier().equals(destination.dimension())
|
||||
&& !properties.has(LinkProperties.INTRA_LINKING)) {
|
||||
return "Same-dimension linking requires an Intra-Linking page";
|
||||
}
|
||||
ServerLevel target = player.level().getServer().getLevel(
|
||||
@@ -61,11 +66,41 @@ public final class LinkingBookItem extends Item {
|
||||
destination.z() - player.getZ()))) {
|
||||
return "Linked destination is obstructed";
|
||||
}
|
||||
ServerLevel source = player.level();
|
||||
Vec3 departure = player.position();
|
||||
Vec3 incoming = player.getDeltaMovement();
|
||||
boolean moved = player.teleportTo(target, destination.x(), destination.y(), destination.z(),
|
||||
Set.of(), destination.yaw(), destination.pitch(), true);
|
||||
if (moved) applySuccessfulLink(player, properties, source, departure, incoming, destination.yaw());
|
||||
return moved ? null : "Teleportation failed";
|
||||
}
|
||||
|
||||
/** Link Panel effects are shared by Linking and Descriptive Books. */
|
||||
public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties,
|
||||
ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) {
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) {
|
||||
double speed = Math.hypot(incoming.x, incoming.z);
|
||||
double yaw = Math.toRadians(destinationYaw);
|
||||
player.setDeltaMovement(speed * Math.cos(yaw), incoming.y + 0.2,
|
||||
speed * Math.sin(yaw));
|
||||
} else {
|
||||
player.setDeltaMovement(Vec3.ZERO);
|
||||
}
|
||||
if (properties.has(LinkProperties.DISARM)) {
|
||||
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) {
|
||||
ItemStack carried = player.getInventory().getItem(slot);
|
||||
if (carried.isEmpty()) continue;
|
||||
player.getInventory().setItem(slot, ItemStack.EMPTY);
|
||||
ItemEntity dropped = new ItemEntity(source, departure.x, departure.y, departure.z,
|
||||
carried.copy());
|
||||
dropped.setDefaultPickUpDelay();
|
||||
source.addFreshEntity(dropped);
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
@@ -73,5 +108,12 @@ public final class LinkingBookItem extends Item {
|
||||
tooltip.accept(Component.literal(target == null ? "Unbound (26.3 port preview)"
|
||||
: "Destination: " + target.dimension() + " (" + (int) target.x() + ", "
|
||||
+ (int) target.y() + ", " + (int) target.z() + ")"));
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.maintain_momentum"));
|
||||
if (properties.has(LinkProperties.INTRA_LINKING))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.intra_linking"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** One Page item with a persistent Link Panel variant; symbol writing is not ported yet. */
|
||||
public final class PageItem extends Item {
|
||||
public PageItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static ItemStack createLinkPanelPage() {
|
||||
return createLinkPanelPage(0);
|
||||
}
|
||||
|
||||
public static ItemStack createLinkPanelPage(int flags) {
|
||||
ItemStack stack = new ItemStack(Rivencraft.PAGE.get());
|
||||
stack.set(Rivencraft.LINK_PANEL_PAGE.get(), true);
|
||||
if (flags != 0) stack.set(Rivencraft.LINK_PROPERTIES.get(), new LinkProperties(flags));
|
||||
return stack;
|
||||
}
|
||||
|
||||
public static boolean isLinkPanel(ItemStack stack) {
|
||||
return stack.is(Rivencraft.PAGE.get()) && stack.getOrDefault(Rivencraft.LINK_PANEL_PAGE.get(), false);
|
||||
}
|
||||
|
||||
public static ItemStack createSymbolPage(Identifier id) {
|
||||
if (PortSymbolPages.find(id) == null)
|
||||
throw new IllegalArgumentException("Unknown bundled symbol " + id);
|
||||
ItemStack stack = new ItemStack(Rivencraft.PAGE.get());
|
||||
stack.set(Rivencraft.SYMBOL_PAGE_ID.get(), id);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
Component customName = stack.get(DataComponents.CUSTOM_NAME);
|
||||
if (customName != null) return customName;
|
||||
Identifier symbolId = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbolId != null) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(symbolId);
|
||||
if (entry != null) {
|
||||
Component name = Component.translatableWithFallback(
|
||||
entry.nameKey(), PortSymbolPages.fallbackName(symbolId));
|
||||
return isCorrupted(symbolId) ? name.copy().withStyle(ChatFormatting.OBFUSCATED) : name;
|
||||
}
|
||||
return Component.translatableWithFallback("item.rivencraft.page.unresolved", "Unknown Symbol Page");
|
||||
}
|
||||
return Component.translatable(isLinkPanel(stack)
|
||||
? "item.rivencraft.page.link_panel" : "item.rivencraft.page.blank");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
Identifier symbolId = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbolId != null) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(symbolId);
|
||||
if (entry != null) {
|
||||
Component description = Component.translatableWithFallback(
|
||||
entry.descriptionKey(), PortSymbolPages.fallbackName(symbolId));
|
||||
tooltip.accept(isCorrupted(symbolId)
|
||||
? description.copy().withStyle(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED)
|
||||
: description.copy().withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
tooltip.accept(Component.translatable("item.rivencraft.page.symbol.port_status")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
return;
|
||||
}
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.maintain_momentum"));
|
||||
if (properties.has(LinkProperties.INTRA_LINKING))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.intra_linking"));
|
||||
tooltip.accept(Component.translatable(isLinkPanel(stack)
|
||||
? "item.rivencraft.page.link_panel.port_status"
|
||||
: "item.rivencraft.page.blank.port_status"));
|
||||
}
|
||||
|
||||
private static boolean isCorrupted(Identifier id) {
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID) && id.getPath().startsWith("corrupted/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
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.ContainerInput;
|
||||
import net.minecraft.world.inventory.DataSlot;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Server-owned virtual output; taking it spends paper and the configured ink cost once. */
|
||||
public final class InkMixerMenu extends AbstractContainerMenu {
|
||||
public static final int MODIFIER = InkMixerBlockEntity.SIZE;
|
||||
public static final int RESULT = MODIFIER + 1;
|
||||
private static final int PLAYER_START = RESULT + 1;
|
||||
private final InkMixerBlockEntity mixer;
|
||||
private final Container inputs;
|
||||
private final SimpleContainer result = new SimpleContainer(1);
|
||||
private final boolean clientSide;
|
||||
private int inkRemaining;
|
||||
private final int[] modifierChances = new int[3];
|
||||
|
||||
public InkMixerMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
instanceof InkMixerBlockEntity found ? found : null);
|
||||
}
|
||||
|
||||
public InkMixerMenu(int id, Inventory inventory, InkMixerBlockEntity mixer) {
|
||||
super(Rivencraft.INK_MIXER_MENU.get(), id);
|
||||
this.mixer = mixer;
|
||||
inputs = mixer == null ? new SimpleContainer(InkMixerBlockEntity.SIZE) : mixer;
|
||||
clientSide = inventory.player.level().isClientSide();
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() { return clientSide ? inkRemaining : mixer.inkRemaining(); }
|
||||
@Override public void set(int value) { inkRemaining = value; }
|
||||
});
|
||||
for (int index = 0; index < modifierChances.length; index++) {
|
||||
final int chanceIndex = index;
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide ? modifierChances[chanceIndex] : mixer.modifierChancePermille(chanceIndex);
|
||||
}
|
||||
@Override public void set(int value) { modifierChances[chanceIndex] = value; }
|
||||
});
|
||||
}
|
||||
addSlot(new MixerSlot(InkMixerBlockEntity.INK, 8, 27));
|
||||
addSlot(new MixerSlot(InkMixerBlockEntity.PAPER, 8, 48));
|
||||
addSlot(new MixerSlot(InkMixerBlockEntity.BOTTLES, 152, 27));
|
||||
addSlot(new ModifierSlot(79, 40));
|
||||
addSlot(new ResultSlot(152, 48));
|
||||
for (int row = 0; row < 3; row++)
|
||||
for (int column = 0; column < 9; column++)
|
||||
addSlot(new Slot(inventory, column + row * 9 + 9, 8 + column * 18, 99 + row * 18));
|
||||
for (int column = 0; column < 9; column++)
|
||||
addSlot(new Slot(inventory, column, 8 + column * 18, 157));
|
||||
refreshResult();
|
||||
}
|
||||
|
||||
public int inkRemaining() { return inkRemaining; }
|
||||
public int modifierChancePermille(int index) { return modifierChances[index]; }
|
||||
|
||||
@Override public void clicked(int slotId, int button, ContainerInput clickType, Player player) {
|
||||
if (slotId == MODIFIER) {
|
||||
if (!clientSide && stillValid(player) && clickType == ContainerInput.PICKUP
|
||||
&& (button == 0 || button == 1)) {
|
||||
ItemStack carried = getCarried();
|
||||
int amount = button == 1 ? 1 : carried.getCount();
|
||||
if (mixer.addModifier(carried, amount)) {
|
||||
setCarried(carried);
|
||||
refreshResult();
|
||||
broadcastChanges();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
super.clicked(slotId, button, clickType, player);
|
||||
// In 26.3, recompute the virtual result in the same packet that changes
|
||||
// paper or ink. Waiting for a later menu update can require a second click.
|
||||
if (!clientSide) refreshResult();
|
||||
}
|
||||
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return mixer != null && mixer.stillValid(player);
|
||||
}
|
||||
|
||||
@Override public void broadcastChanges() {
|
||||
refreshResult();
|
||||
super.broadcastChanges();
|
||||
}
|
||||
|
||||
private void refreshResult() {
|
||||
if (clientSide) return;
|
||||
ItemStack preview = mixer == null ? ItemStack.EMPTY : mixer.previewLinkPanel();
|
||||
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
|
||||
}
|
||||
|
||||
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||
if (index == RESULT) {
|
||||
refreshResult();
|
||||
if (!mixer.canCreateLinkPanel()) return ItemStack.EMPTY;
|
||||
ItemStack output = result.getItem(0).copy();
|
||||
ItemStack remainder = output.copy();
|
||||
if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty())
|
||||
return ItemStack.EMPTY;
|
||||
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
||||
refreshResult();
|
||||
return output;
|
||||
}
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
ItemStack source = slot.getItem();
|
||||
ItemStack original = source.copy();
|
||||
boolean moved;
|
||||
if (index < MODIFIER) moved = moveItemStackTo(source, PLAYER_START, slots.size(), true);
|
||||
else if (source.is(Rivencraft.INK_VIAL.get()))
|
||||
moved = moveItemStackTo(source, InkMixerBlockEntity.INK, InkMixerBlockEntity.INK + 1, false);
|
||||
else if (source.is(Items.PAPER))
|
||||
moved = moveItemStackTo(source, InkMixerBlockEntity.PAPER, InkMixerBlockEntity.PAPER + 1, false);
|
||||
else moved = false;
|
||||
if (!moved) return ItemStack.EMPTY;
|
||||
if (source.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||
else slot.setChanged();
|
||||
refreshResult();
|
||||
return original;
|
||||
}
|
||||
|
||||
private final class MixerSlot extends Slot {
|
||||
MixerSlot(int index, int x, int y) { super(inputs, index, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return inputs.canPlaceItem(getContainerSlot(), stack); }
|
||||
}
|
||||
|
||||
private static final class ModifierSlot extends Slot {
|
||||
ModifierSlot(int x, int y) { super(new SimpleContainer(1), 0, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
@Override public boolean mayPickup(Player player) { return false; }
|
||||
}
|
||||
|
||||
private final class ResultSlot extends Slot {
|
||||
ResultSlot(int x, int y) { super(result, 0, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
@Override public boolean mayPickup(Player player) {
|
||||
return clientSide ? hasItem() : stillValid(player) && mixer.canCreateLinkPanel();
|
||||
}
|
||||
@Override public void onTake(Player player, ItemStack stack) {
|
||||
if (!clientSide && mixer.consumeLinkPanelMaterials()) {
|
||||
refreshResult();
|
||||
}
|
||||
super.onTake(player, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
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.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Binder inputs persist in the block; assembling places the finished book on the cursor. */
|
||||
public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
public static final int STRIP_CLICK_BASE = 3000;
|
||||
private final Container binder;
|
||||
private final PortBookBinderBlockEntity blockEntity;
|
||||
private final String initialTitle;
|
||||
|
||||
public PortBookBinderMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
instanceof PortBookBinderBlockEntity found ? found : null, data.readUtf(32));
|
||||
}
|
||||
|
||||
public PortBookBinderMenu(int id, Inventory inventory, PortBookBinderBlockEntity blockEntity) {
|
||||
this(id, inventory, blockEntity, blockEntity == null ? "" : blockEntity.title());
|
||||
}
|
||||
|
||||
private PortBookBinderMenu(int id, Inventory inventory, PortBookBinderBlockEntity blockEntity, String title) {
|
||||
super(Rivencraft.BOOK_BINDER_MENU.get(), id);
|
||||
this.blockEntity = blockEntity;
|
||||
initialTitle = title;
|
||||
binder = blockEntity == null ? new SimpleContainer(PortBookBinderBlockEntity.SIZE) : blockEntity;
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.COVER, 8, 27) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return stack.is(Items.LEATHER); }
|
||||
});
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.PANEL, -1000, -1000) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return PageItem.isLinkPanel(stack); }
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
});
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.OUTPUT, 152, 27) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
});
|
||||
for (int page = 0; page < PortBookBinderBlockEntity.PAGE_COUNT; page++) {
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.FIRST_PAGE + page, -1000, -1000) {
|
||||
@Override public boolean mayPlace(ItemStack stack) {
|
||||
return binder.canPlaceItem(getContainerSlot(), stack);
|
||||
}
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
});
|
||||
}
|
||||
addStandardInventorySlots(inventory, 8, 99);
|
||||
}
|
||||
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return blockEntity != null && blockEntity.stillValid(player);
|
||||
}
|
||||
|
||||
public String title() { return initialTitle; }
|
||||
|
||||
@Override public boolean clickMenuButton(Player player, int id) {
|
||||
if (id < STRIP_CLICK_BASE || id > STRIP_CLICK_BASE + PortBookBinderBlockEntity.PAGE_COUNT
|
||||
|| !stillValid(player)) return false;
|
||||
if (player.level().isClientSide()) return true;
|
||||
int index = id - STRIP_CLICK_BASE;
|
||||
ItemStack carried = getCarried();
|
||||
if (!carried.isEmpty()) {
|
||||
if (index == 0) {
|
||||
if (!PageItem.isLinkPanel(carried)
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty()) return false;
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, carried.copyWithCount(1));
|
||||
} else if (!blockEntity.insertPage(index - 1, carried.copyWithCount(1))) return false;
|
||||
ItemStack remainder = carried.copy();
|
||||
remainder.shrink(1);
|
||||
setCarried(remainder);
|
||||
} else {
|
||||
ItemStack removed = index == 0
|
||||
? binder.removeItem(PortBookBinderBlockEntity.PANEL, 1)
|
||||
: blockEntity.takePage(index - 1);
|
||||
if (removed.isEmpty()) return false;
|
||||
setCarried(removed);
|
||||
}
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setTitle(ServerPlayer player, String value) {
|
||||
if (stillValid(player) && value != null && value.length() <= PortBookNetworking.MAX_BOOK_TITLE_LENGTH)
|
||||
blockEntity.setTitle(value);
|
||||
}
|
||||
|
||||
public void assemble(ServerPlayer player, String title) {
|
||||
if (!stillValid(player) || title.isBlank() || title.length() > PortBookNetworking.MAX_BOOK_TITLE_LENGTH
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.OUTPUT).isEmpty()
|
||||
|| !PortAuthoredWriting.supports(blockEntity.symbolPages())
|
||||
|| !getCarried().isEmpty()) return;
|
||||
ItemStack cover = binder.getItem(PortBookBinderBlockEntity.COVER);
|
||||
ItemStack panel = binder.getItem(PortBookBinderBlockEntity.PANEL);
|
||||
if (!cover.is(Items.LEATHER) || !PageItem.isLinkPanel(panel)) return;
|
||||
blockEntity.setTitle(title);
|
||||
ItemStack book = new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
||||
if (id == null) return;
|
||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
if (blockEntity.hasSymbolPages())
|
||||
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
||||
var modifiers = panel.get(Rivencraft.LINK_PROPERTIES.get());
|
||||
if (modifiers != null) book.set(Rivencraft.LINK_PROPERTIES.get(), modifiers);
|
||||
cover.shrink(1);
|
||||
panel.shrink(1);
|
||||
binder.setItem(PortBookBinderBlockEntity.COVER, cover);
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
||||
blockEntity.clearSymbolPages();
|
||||
setCarried(book);
|
||||
broadcastChanges();
|
||||
}
|
||||
|
||||
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
if (index >= PortBookBinderBlockEntity.FIRST_PAGE && index < PortBookBinderBlockEntity.SIZE) {
|
||||
ItemStack page = slot.getItem().copy();
|
||||
ItemStack remainder = page.copy();
|
||||
if (!moveItemStackTo(remainder, PortBookBinderBlockEntity.SIZE, slots.size(), true)
|
||||
|| !remainder.isEmpty()) return ItemStack.EMPTY;
|
||||
blockEntity.takePage(index - PortBookBinderBlockEntity.FIRST_PAGE);
|
||||
broadcastChanges();
|
||||
return page;
|
||||
}
|
||||
ItemStack stack = slot.getItem();
|
||||
ItemStack original = stack.copy();
|
||||
boolean moved;
|
||||
if (index < PortBookBinderBlockEntity.SIZE) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.SIZE, slots.size(), true);
|
||||
} else if (stack.is(Items.LEATHER)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.COVER,
|
||||
PortBookBinderBlockEntity.COVER + 1, false);
|
||||
} else if (PageItem.isLinkPanel(stack)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.PANEL,
|
||||
PortBookBinderBlockEntity.PANEL + 1, false);
|
||||
} else if (stack.is(Rivencraft.PAGE.get()) && stack.has(Rivencraft.SYMBOL_PAGE_ID.get())) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.FIRST_PAGE,
|
||||
PortBookBinderBlockEntity.SIZE, false);
|
||||
} else return ItemStack.EMPTY;
|
||||
if (!moved) return ItemStack.EMPTY;
|
||||
if (stack.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||
else slot.setChanged();
|
||||
slot.onTake(player, stack);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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.Identifier;
|
||||
|
||||
public record AssembleBinderPayload(String title) implements CustomPacketPayload {
|
||||
public static final Type<AssembleBinderPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "assemble_binder"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AssembleBinderPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.stringUtf8(32), AssembleBinderPayload::title,
|
||||
AssembleBinderPayload::new);
|
||||
@Override public Type<AssembleBinderPayload> type() { return TYPE; }
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -13,8 +16,11 @@ import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
|
||||
import java.util.Locale;
|
||||
import java.util.List;
|
||||
|
||||
/** Server-authoritative handheld Linking Book binding and travel. */
|
||||
public final class PortBookNetworking {
|
||||
@@ -41,6 +47,21 @@ public final class PortBookNetworking {
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) enterAge(player, payload);
|
||||
});
|
||||
registrar.playToServer(AssembleBinderPayload.TYPE, AssembleBinderPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player
|
||||
&& player.containerMenu instanceof PortBookBinderMenu menu) {
|
||||
menu.assemble(player, payload.title().strip());
|
||||
}
|
||||
});
|
||||
registrar.playToServer(SetBinderTitlePayload.TYPE, SetBinderTitlePayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player
|
||||
&& player.containerMenu instanceof PortBookBinderMenu menu
|
||||
&& menu.containerId == payload.containerId()) {
|
||||
menu.setTitle(player, payload.title());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void bind(ServerPlayer player, BindLinkingBookPayload payload) {
|
||||
@@ -59,7 +80,7 @@ public final class PortBookNetworking {
|
||||
private static void use(ServerPlayer player, UseLinkingBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.LINKING_BOOK.get())) return;
|
||||
String failure = LinkingBookItem.travel(player, stack.get(Rivencraft.LINK_BOOK_TARGET.get()));
|
||||
String failure = LinkingBookItem.travel(player, stack);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
}
|
||||
|
||||
@@ -68,6 +89,30 @@ public final class PortBookNetworking {
|
||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get()) || stack.has(Rivencraft.AGE_BOOK_ID.get())) return;
|
||||
String title = payload.title().strip();
|
||||
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
||||
Identifier id = createAge(player, title);
|
||||
if (id == null) return;
|
||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
player.sendOverlayMessage(Component.literal("Bound Descriptive Book: " + title));
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
|
||||
/** Shared name resolution and Age creation for handheld books and the Binder. */
|
||||
public static Identifier createAge(ServerPlayer player, String title) {
|
||||
return createAge(player, title, List.of());
|
||||
}
|
||||
|
||||
public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
|
||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
try {
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
}
|
||||
String base = title.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]+", "_")
|
||||
.replaceAll("^[_\\.-]+|[_\\.-]+$", "");
|
||||
if (base.isEmpty()) base = "age";
|
||||
@@ -80,17 +125,13 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong());
|
||||
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong(), biomePlan,
|
||||
PortAuthoredWriting.terrain(pages));
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
player.sendOverlayMessage(Component.literal("Bound Descriptive Book: " + title));
|
||||
player.containerMenu.broadcastChanges();
|
||||
return id;
|
||||
}
|
||||
|
||||
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
|
||||
@@ -98,8 +139,14 @@ public final class PortBookNetworking {
|
||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
||||
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (id == null) return;
|
||||
ServerLevel source = player.level();
|
||||
Vec3 departure = player.position();
|
||||
Vec3 incoming = player.getDeltaMovement();
|
||||
String failure = PortAgeTravel.enter(player, id);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
else LinkingBookItem.applySuccessfulLink(player,
|
||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||
source, departure, incoming, player.getYRot());
|
||||
}
|
||||
|
||||
private static InteractionHand hand(boolean offhand) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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.Identifier;
|
||||
|
||||
public record SetBinderTitlePayload(int containerId, String title) implements CustomPacketPayload {
|
||||
public static final Type<SetBinderTitlePayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_binder_title"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, SetBinderTitlePayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.VAR_INT, SetBinderTitlePayload::containerId,
|
||||
ByteBufCodecs.stringUtf8(32), SetBinderTitlePayload::title,
|
||||
SetBinderTitlePayload::new);
|
||||
@Override public Type<SetBinderTitlePayload> type() { return TYPE; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Only items already ported to 26.3 appear here. */
|
||||
public final class ModCreativeTabs {
|
||||
private static final DeferredRegister<CreativeModeTab> TABS =
|
||||
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
TABS.register("rivencraft", () -> CreativeModeTab.builder()
|
||||
.title(Component.translatable("itemGroup.rivencraft"))
|
||||
.icon(() -> Rivencraft.DESCRIPTIVE_BOOK.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
output.accept(Rivencraft.LINKING_BOOK.get());
|
||||
output.accept(Rivencraft.CRYSTAL_BLOCK_ITEM.get());
|
||||
output.accept(Rivencraft.BUDDING_CRYSTAL_ITEM.get());
|
||||
output.accept(Rivencraft.SMALL_CRYSTAL_BUD_ITEM.get());
|
||||
output.accept(Rivencraft.MEDIUM_CRYSTAL_BUD_ITEM.get());
|
||||
output.accept(Rivencraft.LARGE_CRYSTAL_BUD_ITEM.get());
|
||||
output.accept(Rivencraft.CRYSTAL_CLUSTER_ITEM.get());
|
||||
output.accept(Rivencraft.CRYSTAL_SHARD.get());
|
||||
output.accept(Rivencraft.INK_VIAL.get());
|
||||
output.accept(Rivencraft.BOOK_BINDER_ITEM.get());
|
||||
output.accept(Rivencraft.INK_MIXER_ITEM.get());
|
||||
})
|
||||
.build());
|
||||
TABS.register("rivencraft_link_pages", () -> CreativeModeTab.builder()
|
||||
.title(Component.translatable("itemGroup.rivencraft.pages.link"))
|
||||
.icon(PageItem::createLinkPanelPage)
|
||||
.displayItems((parameters, output) -> {
|
||||
int[] flags = {LinkProperties.DISARM, LinkProperties.MAINTAIN_MOMENTUM,
|
||||
LinkProperties.INTRA_LINKING};
|
||||
for (int combination = 0; combination < 8; combination++) {
|
||||
int selected = 0;
|
||||
for (int bit = 0; bit < flags.length; bit++)
|
||||
if ((combination & (1 << bit)) != 0) selected |= flags[bit];
|
||||
output.accept(PageItem.createLinkPanelPage(selected));
|
||||
}
|
||||
})
|
||||
.build());
|
||||
for (String category : PortSymbolPages.categories()) {
|
||||
String path = category.substring(category.indexOf(':') + 1);
|
||||
TABS.register("rivencraft_pages_" + path, () -> CreativeModeTab.builder()
|
||||
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + path,
|
||||
"Rivencraft " + PortSymbolPages.fallbackName(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path))
|
||||
+ " Pages"))
|
||||
.icon(() -> PortSymbolPages.icon(category))
|
||||
.displayItems((parameters, output) ->
|
||||
PortSymbolPages.pages(category, parameters.holders()).forEach(output::accept))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private ModCreativeTabs() {
|
||||
}
|
||||
|
||||
public static void register(IEventBus modEventBus) {
|
||||
TABS.register(modEventBus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortScaledBiomeSource;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Serializes the custom biome resolver in saved 26.3 Age level stems. */
|
||||
public final class PortBiomeSources {
|
||||
private static final DeferredRegister<MapCodec<? extends BiomeSource>> SOURCES =
|
||||
DeferredRegister.create(Registries.BIOME_SOURCE, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
SOURCES.register("scaled", () -> PortScaledBiomeSource.CODEC);
|
||||
}
|
||||
|
||||
private PortBiomeSources() {}
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
SOURCES.register(bus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Built-in Legacy ink additives, carried forward from the 1.21.1 mixer. */
|
||||
public final class InkModifierIngredients {
|
||||
public record Effect(int flag, double chance) {}
|
||||
public record Ingredient(ItemStack item, List<Effect> effects) {}
|
||||
|
||||
private static final List<Ingredient> BUILT_IN = List.of(
|
||||
new Ingredient(new ItemStack(Items.GUNPOWDER), List.of(new Effect(LinkProperties.DISARM, 0.20))),
|
||||
new Ingredient(new ItemStack(Items.MUSHROOM_STEW), List.of(new Effect(LinkProperties.DISARM, 0.05))),
|
||||
new Ingredient(new ItemStack(Items.EXPERIENCE_BOTTLE), List.of(new Effect(LinkProperties.INTRA_LINKING, 0.15))),
|
||||
new Ingredient(new ItemStack(Items.ENDER_PEARL), List.of(
|
||||
new Effect(LinkProperties.INTRA_LINKING, 0.15),
|
||||
new Effect(LinkProperties.DISARM, 0.15))),
|
||||
new Ingredient(new ItemStack(Items.FEATHER), List.of(new Effect(LinkProperties.MAINTAIN_MOMENTUM, 0.15))),
|
||||
new Ingredient(new ItemStack(Items.FIRE_CHARGE), List.of(new Effect(LinkProperties.DISARM, 0.25)))
|
||||
);
|
||||
|
||||
private InkModifierIngredients() {}
|
||||
|
||||
public static List<Ingredient> builtIn() { return BUILT_IN; }
|
||||
|
||||
public static Ingredient find(ItemStack stack) {
|
||||
return BUILT_IN.stream().filter(ingredient -> stack.is(ingredient.item().getItem())).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Shared, server-side vial-to-meter exchange from the 1.21.1 Desk and Mixer. */
|
||||
public final class InkVialExchange {
|
||||
private InkVialExchange() {}
|
||||
|
||||
public static boolean canPay(NonNullList<ItemStack> items, int inkSlot, int bottleSlot,
|
||||
int storedInk, int cost) {
|
||||
return storedInk >= cost || canExchange(items, inkSlot, bottleSlot)
|
||||
&& storedInk + vialInk(items.get(inkSlot)) >= cost;
|
||||
}
|
||||
|
||||
/** Returns added ink; zero means no inputs were changed. */
|
||||
public static int exchange(NonNullList<ItemStack> items, int inkSlot, int bottleSlot) {
|
||||
if (!canExchange(items, inkSlot, bottleSlot)) return 0;
|
||||
ItemStack vial = items.get(inkSlot);
|
||||
int addedInk = vialInk(vial);
|
||||
vial.shrink(1);
|
||||
if (vial.isEmpty()) items.set(inkSlot, ItemStack.EMPTY);
|
||||
ItemStack bottles = items.get(bottleSlot);
|
||||
if (bottles.isEmpty()) items.set(bottleSlot, new ItemStack(Items.GLASS_BOTTLE));
|
||||
else bottles.grow(1);
|
||||
return addedInk;
|
||||
}
|
||||
|
||||
private static boolean canExchange(NonNullList<ItemStack> items, int inkSlot, int bottleSlot) {
|
||||
ItemStack vial = items.get(inkSlot);
|
||||
ItemStack bottles = items.get(bottleSlot);
|
||||
return vial.is(Rivencraft.INK_VIAL.get()) && vialInk(vial) > 0
|
||||
&& (bottles.isEmpty() || bottles.is(Items.GLASS_BOTTLE)
|
||||
&& bottles.getCount() < bottles.getMaxStackSize());
|
||||
}
|
||||
|
||||
private static int vialInk(ItemStack vial) {
|
||||
return Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.crafting.CraftingInput;
|
||||
import net.minecraft.world.item.crafting.CustomRecipe;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Legacy's shapeless leather + Link Panel recipe; never accepts a blank Page. */
|
||||
public final class LinkingBookRecipe extends CustomRecipe {
|
||||
private static final LinkingBookRecipe INSTANCE = new LinkingBookRecipe();
|
||||
public static final MapCodec<LinkingBookRecipe> MAP_CODEC = MapCodec.unit(INSTANCE);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, LinkingBookRecipe> STREAM_CODEC = StreamCodec.unit(INSTANCE);
|
||||
public static final RecipeSerializer<LinkingBookRecipe> SERIALIZER = new RecipeSerializer<>(MAP_CODEC, STREAM_CODEC);
|
||||
|
||||
private LinkingBookRecipe() {
|
||||
}
|
||||
|
||||
private static ItemStack findPanel(CraftingInput input) {
|
||||
if (input.ingredientCount() != 2) return ItemStack.EMPTY;
|
||||
ItemStack panel = ItemStack.EMPTY;
|
||||
boolean leather = false;
|
||||
for (ItemStack stack : input.items()) {
|
||||
if (stack.isEmpty()) continue;
|
||||
if (PageItem.isLinkPanel(stack) && panel.isEmpty()) {
|
||||
panel = stack;
|
||||
} else if (stack.is(Items.LEATHER) && !leather) {
|
||||
leather = true;
|
||||
} else {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
return leather ? panel : ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingInput input, Level level) {
|
||||
return !findPanel(input).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingInput input) {
|
||||
ItemStack panel = findPanel(input);
|
||||
if (panel.isEmpty()) return ItemStack.EMPTY;
|
||||
ItemStack book = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
if (panel.has(DataComponents.CUSTOM_NAME)) {
|
||||
book.set(DataComponents.CUSTOM_NAME, panel.get(DataComponents.CUSTOM_NAME));
|
||||
}
|
||||
LinkProperties properties = panel.get(Rivencraft.LINK_PROPERTIES.get());
|
||||
if (properties != null) book.set(Rivencraft.LINK_PROPERTIES.get(), properties);
|
||||
return book;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<LinkingBookRecipe> getSerializer() {
|
||||
return SERIALIZER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
|
||||
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
|
||||
public final class PortAuthoredWriting {
|
||||
private static final int MAX_AUTHORED_BIOMES = 252;
|
||||
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/standard");
|
||||
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/amplified");
|
||||
private static final Identifier ISLAND_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/island");
|
||||
|
||||
private PortAuthoredWriting() {}
|
||||
|
||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||
public static boolean supports(List<Identifier> pages) {
|
||||
boolean terrain = false;
|
||||
int biomes = 0;
|
||||
int controllers = 0;
|
||||
for (Identifier page : pages) {
|
||||
if ((page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
||||
|| page.equals(ISLAND_TERRAIN)) && !terrain) {
|
||||
terrain = true;
|
||||
} else if (controller(page) != null && controllers++ == 0) {
|
||||
// An explicit controller needs an authored pool.
|
||||
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
||||
&& PortSymbolPages.find(page) != null
|
||||
&& PortSymbolPages.find(page).category().equals("rivencraft:biome")
|
||||
&& PortRegistryPages.biomeId(page) != null) {
|
||||
biomes++;
|
||||
} else return false;
|
||||
}
|
||||
if (controllers != 0) {
|
||||
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
||||
.filter(value -> value != null).findFirst().orElseThrow();
|
||||
return switch (distribution) {
|
||||
case NATIVE -> biomes == 0;
|
||||
case SINGLE -> biomes == 1;
|
||||
default -> biomes >= 2;
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static PortBiomePlan.Distribution controller(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "biome/native" -> PortBiomePlan.Distribution.NATIVE;
|
||||
case "biome/single" -> PortBiomePlan.Distribution.SINGLE;
|
||||
case "biome/tiled" -> PortBiomePlan.Distribution.TILED;
|
||||
case "biome/grid" -> PortBiomePlan.Distribution.GRID;
|
||||
case "biome/tiny" -> PortBiomePlan.Distribution.TINY;
|
||||
case "biome/small" -> PortBiomePlan.Distribution.SMALL;
|
||||
case "biome/medium" -> PortBiomePlan.Distribution.MEDIUM;
|
||||
case "biome/large" -> PortBiomePlan.Distribution.LARGE;
|
||||
case "biome/huge" -> PortBiomePlan.Distribution.HUGE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortAgeTerrain terrain(List<Identifier> pages) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
if (pages.contains(AMPLIFIED_TERRAIN)) return PortAgeTerrain.AMPLIFIED;
|
||||
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
||||
return PortAgeTerrain.STANDARD;
|
||||
}
|
||||
|
||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Only Standard/Amplified/Island terrain with supported Biome pages is available in this port slice");
|
||||
List<Identifier> biomes = new ArrayList<>();
|
||||
PortBiomePlan.Distribution distribution = null;
|
||||
for (Identifier page : pages) {
|
||||
if (page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
||||
|| page.equals(ISLAND_TERRAIN)) continue;
|
||||
PortBiomePlan.Distribution selected = controller(page);
|
||||
if (selected != null) { distribution = selected; continue; }
|
||||
Identifier biome = PortRegistryPages.biomeId(page);
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
if (registry.get(ResourceKey.create(Registries.BIOME, biome)).isEmpty())
|
||||
throw new IllegalArgumentException("Unknown authored biome: " + biome);
|
||||
biomes.add(biome);
|
||||
}
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new PortBiomePlan(distribution, List.of());
|
||||
if (biomes.isEmpty()) return null;
|
||||
if (distribution == null) distribution = biomes.size() == 1
|
||||
? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
|
||||
? PortBiomePlan.Distribution.TILED : PortBiomePlan.Distribution.MEDIUM;
|
||||
return new PortBiomePlan(distribution, biomes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
|
||||
/** Registry-derived 26.3 biome and material pages, with the same IDs as 1.21.1. */
|
||||
public final class PortRegistryPages {
|
||||
private static final TagKey<net.minecraft.world.level.biome.Biome> CAVE_BIOMES = TagKey.create(
|
||||
Registries.BIOME, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "cave_biomes"));
|
||||
private static final Identifier DEFERRED_PLACEHOLDER = Identifier.fromNamespaceAndPath(
|
||||
"terrablender", "deferred_placeholder");
|
||||
|
||||
private PortRegistryPages() {}
|
||||
|
||||
public static Identifier biomePageId(Identifier biome) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/"
|
||||
+ (biome.getNamespace().equals("minecraft") ? "" : biome.getNamespace() + "/")
|
||||
+ biome.getPath());
|
||||
}
|
||||
|
||||
public static Identifier blockPageId(Identifier block) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"block/" + block.getNamespace() + "/" + block.getPath());
|
||||
}
|
||||
|
||||
public static PortSymbolPages.Entry find(Identifier symbol) {
|
||||
if (!symbol.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
String path = symbol.getPath();
|
||||
if (path.startsWith("biome/")) {
|
||||
Identifier biome = biomeId(symbol);
|
||||
if (biome == null) return null;
|
||||
String key = biome.getNamespace().equals("minecraft")
|
||||
? "symbol.rivencraft.biome." + biome.getPath()
|
||||
: "biome." + biome.getNamespace() + "." + biome.getPath().replace('/', '.');
|
||||
return new PortSymbolPages.Entry(symbol, "rivencraft:biome", key,
|
||||
"symbol.rivencraft.biome.generated.description",
|
||||
"Nature Form " + glyphWord(biome.getPath(), "World") + " Biome");
|
||||
}
|
||||
if (path.startsWith("block/")) {
|
||||
Identifier block = blockId(symbol);
|
||||
if (block == null) return null;
|
||||
String category = materialCategory(BuiltInRegistries.BLOCK.getValue(block));
|
||||
if (category == null) return null;
|
||||
return new PortSymbolPages.Entry(symbol, category,
|
||||
"block." + block.getNamespace() + "." + block.getPath().replace('/', '_'),
|
||||
"symbol.rivencraft.block.generated.description",
|
||||
"Modifier Constraint " + glyphWord(block.getPath(), "Stone") + " Form");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
||||
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
||||
.filter(holder -> isCave(holder) == caves)
|
||||
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
||||
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
||||
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static List<ItemStack> materialPages(boolean fluids) {
|
||||
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
||||
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
||||
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
||||
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
||||
}
|
||||
|
||||
private static boolean isCave(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
|
||||
String path = holder.key().identifier().getPath();
|
||||
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||
|| path.equals("deep_dark") || path.endsWith("_caves");
|
||||
}
|
||||
|
||||
private static String materialCategory(Block block) {
|
||||
if (block == null || block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR
|
||||
|| block instanceof EntityBlock || block.defaultBlockState().hasBlockEntity()) return null;
|
||||
var state = block.defaultBlockState();
|
||||
var fluid = state.getFluidState();
|
||||
if (fluid.isSource() && fluid.createLegacyBlock().getBlock() == block) return "rivencraft:fluid";
|
||||
return fluid.isEmpty() && state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, BlockPos.ZERO)
|
||||
? "rivencraft:block" : null;
|
||||
}
|
||||
|
||||
public static Identifier biomeId(Identifier symbol) {
|
||||
String path = symbol.getPath().substring("biome/".length());
|
||||
int slash = path.indexOf('/');
|
||||
if (slash < 0) return Identifier.fromNamespaceAndPath("minecraft", path);
|
||||
return Identifier.fromNamespaceAndPath(path.substring(0, slash), path.substring(slash + 1));
|
||||
}
|
||||
|
||||
public static Identifier blockId(Identifier symbol) {
|
||||
String path = symbol.getPath().substring("block/".length());
|
||||
int slash = path.indexOf('/');
|
||||
if (slash < 1) return null;
|
||||
return Identifier.fromNamespaceAndPath(path.substring(0, slash), path.substring(slash + 1));
|
||||
}
|
||||
|
||||
public static String fallbackName(Identifier symbol) {
|
||||
boolean biome = symbol.getPath().startsWith("biome/");
|
||||
Identifier source = biome ? biomeId(symbol) : blockId(symbol);
|
||||
if (source == null) return symbol.toString();
|
||||
String words = Arrays.stream(source.getPath().replace('/', '_').split("_"))
|
||||
.filter(word -> !word.isEmpty())
|
||||
.map(word -> word.substring(0, 1).toUpperCase(Locale.ROOT) + word.substring(1))
|
||||
.reduce((a, b) -> a + " " + b).orElse("Unknown");
|
||||
return words + (biome ? " Biome" : "rivencraft:fluid".equals(find(symbol).category())
|
||||
? " Fluid" : " Material")
|
||||
+ (source.getNamespace().equals("minecraft") ? "" : " (" + source.getNamespace() + ")");
|
||||
}
|
||||
|
||||
private static String glyphWord(String path, String fallback) {
|
||||
String word = Arrays.stream(path.replace('/', '_').split("_"))
|
||||
.map(part -> part.replaceAll("[^A-Za-z]", ""))
|
||||
.filter(part -> !part.isEmpty())
|
||||
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
||||
.reduce("", String::concat);
|
||||
if (word.isEmpty()) word = fallback;
|
||||
return word.length() > 48 ? word.substring(0, 48) : word;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
/** Mystcraft Legacy's layered base-25 numeral components, shared with 1.21.1. */
|
||||
public final class PortSymbolNumerals {
|
||||
private PortSymbolNumerals() {}
|
||||
|
||||
public static int[] componentsForDigit(int digit) {
|
||||
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;
|
||||
if (fifths == 0) return new int[] {55 + remainder};
|
||||
if (remainder == 0) return new int[] {59 + fifths};
|
||||
return new int[] {59 + fifths, 55 + remainder};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashSet;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
|
||||
/** Bundled 1.21.1 symbol identities, shared by the port's creative pages. */
|
||||
public final class PortSymbolPages {
|
||||
public record Entry(Identifier id, String category, String nameKey, String descriptionKey, String poem) {}
|
||||
|
||||
private static final Map<Identifier, Entry> ENTRIES = load();
|
||||
|
||||
private PortSymbolPages() {}
|
||||
|
||||
public static Entry find(Identifier id) {
|
||||
Entry bundled = ENTRIES.get(id);
|
||||
return bundled != null ? bundled : PortRegistryPages.find(id);
|
||||
}
|
||||
|
||||
public static List<String> categories() {
|
||||
LinkedHashSet<String> categories = new LinkedHashSet<>();
|
||||
ENTRIES.values().stream().map(Entry::category).forEach(categories::add);
|
||||
categories.add("rivencraft:cave_biome");
|
||||
categories.add("rivencraft:block");
|
||||
categories.add("rivencraft:fluid");
|
||||
return List.copyOf(categories);
|
||||
}
|
||||
|
||||
public static List<ItemStack> pages(String category, HolderLookup.Provider registries) {
|
||||
List<ItemStack> pages = new ArrayList<>();
|
||||
ENTRIES.values().stream().filter(entry -> entry.category().equals(category))
|
||||
.filter(entry -> !category.equals("rivencraft:biome")
|
||||
|| !entry.id().getPath().startsWith("biome/"))
|
||||
.forEach(entry -> pages.add(PageItem.createSymbolPage(entry.id())));
|
||||
if (category.equals("rivencraft:biome")) pages.addAll(PortRegistryPages.biomePages(registries, false));
|
||||
if (category.equals("rivencraft:cave_biome")) pages.addAll(PortRegistryPages.biomePages(registries, true));
|
||||
if (category.equals("rivencraft:block")) pages.addAll(PortRegistryPages.materialPages(false));
|
||||
if (category.equals("rivencraft:fluid")) pages.addAll(PortRegistryPages.materialPages(true));
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
public static ItemStack icon(String category) {
|
||||
if (category.equals("rivencraft:cave_biome"))
|
||||
return PageItem.createSymbolPage(PortRegistryPages.biomePageId(
|
||||
Identifier.fromNamespaceAndPath("minecraft", "lush_caves")));
|
||||
if (category.equals("rivencraft:block") || category.equals("rivencraft:fluid"))
|
||||
return PageItem.createSymbolPage(PortRegistryPages.blockPageId(
|
||||
Identifier.fromNamespaceAndPath("minecraft",
|
||||
category.equals("rivencraft:fluid") ? "water" : "stone")));
|
||||
return ENTRIES.values().stream().filter(entry -> entry.category().equals(category))
|
||||
.findFirst().map(entry -> PageItem.createSymbolPage(entry.id()))
|
||||
.orElseGet(PageItem::createLinkPanelPage);
|
||||
}
|
||||
|
||||
public static String fallbackName(Identifier id) {
|
||||
if (id.getPath().startsWith("biome/") || id.getPath().startsWith("block/"))
|
||||
return PortRegistryPages.fallbackName(id);
|
||||
String path = id.getPath();
|
||||
String stem = path.substring(path.lastIndexOf('/') + 1).replace('_', ' ');
|
||||
if (stem.isEmpty()) return id.toString();
|
||||
return Character.toUpperCase(stem.charAt(0)) + stem.substring(1);
|
||||
}
|
||||
|
||||
private static Map<Identifier, Entry> load() {
|
||||
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
||||
try (var stream = PortSymbolPages.class.getResourceAsStream(resource)) {
|
||||
if (stream == null) throw new IllegalStateException("Missing generated symbol index " + resource);
|
||||
var json = JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
Map<Identifier, Entry> entries = new LinkedHashMap<>();
|
||||
json.entrySet().forEach(item -> {
|
||||
String[] parts = item.getKey().split(":", 2);
|
||||
if (parts.length != 2 || !parts[0].equals(Rivencraft.MOD_ID))
|
||||
throw new IllegalStateException("Unexpected bundled symbol " + item.getKey());
|
||||
Identifier id = Identifier.fromNamespaceAndPath(parts[0], parts[1]);
|
||||
var definition = item.getValue().getAsJsonObject();
|
||||
String category = definition.get("category").getAsString();
|
||||
if (definition.has("semantics") && definition.getAsJsonObject("semantics")
|
||||
.get("role").getAsString().equals("biome_controller"))
|
||||
category = "rivencraft:biome_generation";
|
||||
entries.put(id, new Entry(id, category, definition.get("name").getAsString(),
|
||||
definition.get("description").getAsString(),
|
||||
definition.has("poem") ? definition.get("poem").getAsString() : ""));
|
||||
});
|
||||
return Collections.unmodifiableMap(entries);
|
||||
} catch (java.io.IOException exception) {
|
||||
throw new IllegalStateException("Could not read bundled symbol index", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "rivencraft:item/book_binder"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/budding_crystal"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/crystal_cluster"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "rivencraft:item/ink_mixer"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/ink_vial"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/large_crystal_bud"}}
|
||||
@@ -1,6 +1,15 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "rivencraft:item/linking_book_bound"
|
||||
"type": "minecraft:condition",
|
||||
"property": "minecraft:has_component",
|
||||
"component": "rivencraft:link_book_target",
|
||||
"on_true": {
|
||||
"type": "minecraft:model",
|
||||
"model": "rivencraft:item/linking_book_bound"
|
||||
},
|
||||
"on_false": {
|
||||
"type": "minecraft:model",
|
||||
"model": "rivencraft:item/linking_book_unwritten"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/medium_crystal_bud"}}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:condition",
|
||||
"property": "minecraft:has_component",
|
||||
"component": "rivencraft:link_panel_page",
|
||||
"on_true": {"type": "minecraft:model", "model": "rivencraft:item/page_link_panel"},
|
||||
"on_false": {
|
||||
"type": "minecraft:condition",
|
||||
"property": "minecraft:has_component",
|
||||
"component": "rivencraft:symbol_page_id",
|
||||
"on_true": {
|
||||
"type": "minecraft:special",
|
||||
"base": "rivencraft:item/page",
|
||||
"model": {"type": "rivencraft:symbol_page"}
|
||||
},
|
||||
"on_false": {"type": "minecraft:model", "model": "rivencraft:item/page"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/small_crystal_bud"}}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"item.rivencraft.crystal_shard": "Crystal Shard",
|
||||
"item.rivencraft.descriptive_book": "Descriptive Book",
|
||||
"item.rivencraft.linking_book": "Linking Book",
|
||||
"block.rivencraft.crystal_block": "Crystal Block"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "rivencraft:item/linking_book_unwritten"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"parent":"minecraft:item/generated","textures":{"layer0":"rivencraft:item/page"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"minecraft:block","random_sequence":"rivencraft:blocks/budding_crystal"}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [{
|
||||
"entries": [{
|
||||
"type": "minecraft:alternatives",
|
||||
"children": [
|
||||
{"type": "minecraft:item", "condition": "minecraft:tool/can_silk_touch", "name": "rivencraft:crystal_cluster"},
|
||||
{"type": "minecraft:alternatives", "children": [
|
||||
{"type": "minecraft:item", "condition": {"type": "minecraft:match_tool", "predicate": {"items": "#minecraft:cluster_max_harvestables"}}, "modifier": [
|
||||
{"type": "minecraft:set_count", "count": 4},
|
||||
{"type": "minecraft:apply_bonus", "enchantment": "minecraft:fortune", "formula": "minecraft:ore_drops"}
|
||||
], "name": "rivencraft:crystal_shard"},
|
||||
{"type": "minecraft:item", "modifier": [
|
||||
{"type": "minecraft:set_count", "count": 2},
|
||||
{"type": "minecraft:explosion_decay"}
|
||||
], "name": "rivencraft:crystal_shard"}
|
||||
]}
|
||||
]
|
||||
}],
|
||||
"rolls": 1
|
||||
}],
|
||||
"random_sequence": "rivencraft:blocks/crystal_cluster"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"minecraft:block","pools":[{"condition":"minecraft:tool/can_silk_touch","entries":[{"type":"minecraft:item","name":"rivencraft:large_crystal_bud"}],"rolls":1}],"random_sequence":"rivencraft:blocks/large_crystal_bud"}
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"minecraft:block","pools":[{"condition":"minecraft:tool/can_silk_touch","entries":[{"type":"minecraft:item","name":"rivencraft:medium_crystal_bud"}],"rolls":1}],"random_sequence":"rivencraft:blocks/medium_crystal_bud"}
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"minecraft:block","pools":[{"condition":"minecraft:tool/can_silk_touch","entries":[{"type":"minecraft:item","name":"rivencraft:small_crystal_bud"}],"rolls":1}],"random_sequence":"rivencraft:blocks/small_crystal_bud"}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": ["III", "PPP", "P P"],
|
||||
"key": {
|
||||
"I": "minecraft:iron_ingot",
|
||||
"P": "#minecraft:planks"
|
||||
},
|
||||
"result": {"id": "rivencraft:book_binder", "count": 1}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": ["SS", "SS"],
|
||||
"key": {
|
||||
"S": "rivencraft:crystal_shard"
|
||||
},
|
||||
"result": {
|
||||
"id": "rivencraft:crystal_block",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"pattern": ["S S", "SVS", "PSP"],
|
||||
"key": {
|
||||
"S": "minecraft:stone",
|
||||
"V": "minecraft:glass_bottle",
|
||||
"P": "#minecraft:planks"
|
||||
},
|
||||
"result": {"id": "rivencraft:ink_mixer", "count": 1}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
"minecraft:black_dye",
|
||||
"minecraft:black_dye",
|
||||
"minecraft:glass_bottle",
|
||||
"minecraft:water_bucket"
|
||||
],
|
||||
"result": {"id": "rivencraft:ink_vial", "count": 1}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"rivencraft:linking_book"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.abandoned_camps",
|
||||
"description": "symbol.rivencraft.feature.abandoned_camps.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:abandoned_camps" },
|
||||
"poem": "Terrain Form Abandoned Camp"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.fallen_poplar_trees",
|
||||
"description": "symbol.rivencraft.feature.fallen_poplar_trees.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:fallen_poplar_trees" },
|
||||
"poem": "Nature Form Poplar Fallen"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.poplar_trees",
|
||||
"description": "symbol.rivencraft.feature.poplar_trees.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:poplar_trees" },
|
||||
"poem": "Nature Form Poplar Growth"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.red_shrubs",
|
||||
"description": "symbol.rivencraft.feature.red_shrubs.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:red_shrubs" },
|
||||
"poem": "Nature Form Red Growth"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.shelf_mushrooms",
|
||||
"description": "symbol.rivencraft.feature.shelf_mushrooms.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:shelf_mushrooms" },
|
||||
"poem": "Nature Form Shelf Fungi"
|
||||
}
|
||||